需要添加直播接口

This commit is contained in:
cbb
2026-01-12 17:52:15 +08:00
parent 83fec2617c
commit 13af9eb303
281 changed files with 313157 additions and 104 deletions

View File

@@ -0,0 +1,11 @@
## 1.1.32025-12-26
- 修复安卓包名冲突问题
## 1.1.22025-12-16
- 修复缺少依赖引起的编译问题
## 1.1.12025-12-16
- 修复与第三方插件同时使用时的编译报错
## 1.1.02025-12-05
- 新增主播 PK 接口
- 修复特定路径下语聊房创建房间失败的问题
## 1.0.02025-10-20
- 支持音视频直播能力

View File

@@ -0,0 +1,571 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }" @click.stop>
<view class="drawer-header">
<text class="drawer-title">音效设置</text>
<text class="drawer-done" @tap="close">完成</text>
</view>
<!-- 耳返开关 -->
<view class="setting-item">
<text class="setting-label">耳返</text>
<switch :checked="earMonitor" @change="handleEarMonitorChange" class="setting-switch" color="#2B65FB"
style="transform:scale(0.7)" />
</view>
<!-- 背景音乐 -->
<!-- <view class="setting-item">
<text class="setting-label">背景音乐</text>
<view class="setting-value" @tap="handleSelectMusic">
<text class="setting-text">选择音乐</text>
<image class="setting-arrow" src="/static/images/right-arrow.png" mode="aspectFit" />
</view>
</view> -->
<!-- 音频设置 -->
<view class="volume-settings">
<text class="section-title">音频设置</text>
<view class="slider-item">
<text class="slider-label">耳返音量</text>
<view class="custom-slider">
<!-- 减号按钮 -->
<view class="control-btn minus-btn" @tap="decreaseMusicVolume">
<text class="btn-text">-</text>
</view>
<!-- 进度条区域 -->
<view class="progress-section">
<!-- 进度条背景 -->
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (musicVolume / 100 * 400) + 'rpx' }"></view>
</view>
<!-- 当前数值显示 -->
<text class="current-value">{{ musicVolume }}</text>
</view>
<!-- 加号按钮 -->
<view class="control-btn plus-btn" @tap="increaseMusicVolume">
<text class="btn-text">+</text>
</view>
</view>
</view>
<!-- <view class="slider-item">
<text class="slider-label">音乐升降调</text>
<text class="slider-value">{{ pitchValue }}</text>
<slider
:value="pitchValue"
@change="handlePitchChange"
min="-12"
max="12"
show-value
activeColor="#2B65FB"
backgroundColor="rgba(255, 255, 255, 0.1)"
block-color="#FFFFFF"
block-size="24"
/>
</view> -->
</view>
<!-- 变声效果 -->
<view class="voice-effects">
<text class="section-title">变声</text>
<view class="effects-grid">
<view v-for="(effect, index) in voiceEffects" :key="index" class="effect-item"
@tap="handleEffectChange(effect.key)">
<view class="effect-icon-container" :class="{ 'effect-active': currentEffect === effect.key }">
<image class="effect-icon" :src="effect.icon" mode="aspectFit" />
</view>
<text class="effect-name">{{ effect.name }}</text>
</view>
</view>
</view>
<!-- 混响效果 -->
<view class="reverb-effects" style="margin-bottom: 40rpx;">
<text class="section-title">混响</text>
<view class="effects-grid">
<view v-for="(effect, index) in reverbEffects" :key="index" class="effect-item"
@tap="handleReverbChange(effect.key)">
<view class="effect-icon-container" :class="{ 'effect-active': currentReverb === effect.key }">
<image class="effect-icon" :src="effect.icon" mode="aspectFit" />
</view>
<text class="effect-name">{{ effect.name }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
watch
} from 'vue';
import {
useAudioEffectState
} from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState'
const {
setVoiceEarMonitorEnable,
setVoiceEarMonitorVolume,
setAudioChangerType,
setAudioReverbType,
isEarMonitorOpened,
earMonitorVolume,
audioChangerType,
audioReverbType
} = useAudioEffectState(uni.$liveID)
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const earMonitor = ref(false);
const musicVolume = ref(0);
const voiceVolume = ref(0);
const pitchValue = ref(0);
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750,
});
const currentEffect = ref('NONE');
const currentReverb = ref('NONE');
const voiceEffects = [{
key: 'NONE',
name: '原声',
icon: '/static/images/no-effect.png'
},
{
key: 'CHILD',
name: '熊孩子',
icon: '/static/images/voice-wild.png'
},
{
key: 'LITTLE_GIRL',
name: '萝莉',
icon: '/static/images/voice-loli.png'
},
{
key: 'MAN',
name: '大叔',
icon: '/static/images/voice-uncle.png'
},
{
key: 'ETHEREAL',
name: '空灵',
icon: '/static/images/voice-ghost.png'
}
];
const reverbEffects = [{
key: 'NONE',
name: '无效果',
icon: '/static/images/no-effect.png'
},
{
key: 'KTV',
name: 'Karaoke',
icon: '/static/images/reverb-ktv.png'
},
{
key: 'METALLIC',
name: '金属声',
icon: '/static/images/reverb-metal.png'
},
{
key: 'DEEP',
name: '低沉',
icon: '/static/images/reverb-bass.png'
},
{
key: 'LOUD',
name: '洪亮',
icon: '/static/images/reverb-bright.png'
}
];
const close = () => {
emit('update:modelValue', false);
};
// 同步状态数据到UI
const syncStateToUI = () => {
try {
console.log('开始同步状态数据...');
console.log('状态管理器中的值:', {
isEarMonitorOpened: isEarMonitorOpened.value,
earMonitorVolume: earMonitorVolume.value,
audioChangerType: audioChangerType.value,
audioReverbType: audioReverbType.value
});
// 同步耳返状态
earMonitor.value = isEarMonitorOpened.value;
// 同步耳返音量
musicVolume.value = earMonitorVolume.value;
// 同步变声类型 - 确保值有效
if (audioChangerType.value && typeof audioChangerType.value === 'string') {
currentEffect.value = audioChangerType.value;
console.log('变声类型同步:', audioChangerType.value);
} else {
console.warn('变声类型无效:', audioChangerType.value);
currentEffect.value = 'NONE';
}
// 同步混响类型 - 确保值有效
if (audioReverbType.value && typeof audioReverbType.value === 'string') {
currentReverb.value = audioReverbType.value;
console.log('混响类型同步:', audioReverbType.value);
} else {
console.warn('混响类型无效:', audioReverbType.value);
currentReverb.value = 'NONE';
}
console.log('同步后的本地状态:', {
earMonitor: earMonitor.value,
musicVolume: musicVolume.value,
voiceVolume: voiceVolume.value,
currentEffect: currentEffect.value,
currentReverb: currentReverb.value
});
} catch (error) {
console.error('同步状态数据失败:', error);
}
};
onMounted(() => {
console.log('AudioEffectPanel mounted');
uni.getSystemInfo({
success: (res) => {
safeArea.value = res.safeArea;
}
});
// 延迟同步状态数据,确保状态管理器已初始化
setTimeout(() => {
syncStateToUI();
}, 100);
});
// 监听抽屉打开状态,每次打开时同步数据
watch(() => props.modelValue, (newVal) => {
if (newVal) {
console.log('抽屉打开,同步状态数据');
// 抽屉打开时同步状态数据
setTimeout(() => {
syncStateToUI();
}, 50);
}
});
const handleEarMonitorChange = (e) => {
earMonitor.value = e.detail.value;
setVoiceEarMonitorEnable({
enable: earMonitor.value
})
};
const handleSelectMusic = () => {
// TODO: 实现选择音乐逻辑
console.log('选择音乐');
};
const handleMusicVolumeChange = (e) => {
musicVolume.value = e.detail.value;
setVoiceEarMonitorVolume({
volume: musicVolume.value
})
};
const handlePitchChange = (e) => {
pitchValue.value = e.detail.value;
};
const handleEffectChange = (effect) => {
console.log('选择效果:', effect, '当前效果:', currentEffect.value);
currentEffect.value = effect;
setAudioChangerType({
changerType: currentEffect.value
})
};
const handleReverbChange = (reverb) => {
console.log('选择混响:', reverb, '当前混响:', currentReverb.value);
currentReverb.value = reverb;
setAudioReverbType({
reverbType: currentReverb.value
})
};
// 音量控制方法
const decreaseMusicVolume = () => {
const newValue = Math.max(0, musicVolume.value - 10)
handleMusicVolumeChange({
detail: {
value: newValue
}
})
}
const increaseMusicVolume = () => {
const newValue = Math.min(100, musicVolume.value + 10)
handleMusicVolumeChange({
detail: {
value: newValue
}
})
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
padding: 32rpx;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
.drawer-title {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
}
.drawer-done {
font-size: 32rpx;
color: #2B65FB;
}
.setting-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 88rpx;
border-bottom-width: 1px;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.setting-label {
font-size: 28rpx;
color: #FFFFFF;
}
.setting-value {
flex-direction: row;
align-items: center;
}
.setting-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-right: 8rpx;
}
.setting-arrow {
width: 24rpx;
height: 24rpx;
}
.volume-settings {
margin-top: 32rpx;
}
.section-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 24rpx;
}
.slider-item {
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
}
.slider-label {
font-size: 28rpx;
color: #FFFFFF;
margin-bottom: 16rpx;
}
/* 自定义控制区域样式 */
.custom-slider {
flex: 1;
flex-direction: row;
align-items: center;
margin: 0 20rpx;
}
.control-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx solid #2b6ad6;
}
.minus-btn {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(43, 106, 214, 0.1);
}
.plus-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #2b6ad6;
}
.btn-text {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
}
.plus-btn .btn-text {
color: #ffffff;
}
.progress-section {
flex: 1;
margin: 0 20rpx;
align-items: center;
flex-direction: row;
justify-content: center;
}
.progress-bar {
width: 400rpx;
height: 8rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4rpx;
position: relative;
overflow: hidden;
margin-right: 16rpx;
}
.progress-fill {
height: 8rpx;
background-color: #2b6ad6;
border-radius: 4rpx;
}
.current-value {
font-size: 24rpx;
color: #ffffff;
font-weight: 600;
text-align: center;
z-index: 10;
}
.voice-effects,
.reverb-effects {
margin-top: 32rpx;
}
.effects-grid {
flex-direction: row;
flex-wrap: wrap;
margin: 0 -8rpx;
}
.effect-item {
margin: 8rpx;
/* background-color: rgba(255, 255, 255, 0.1); */
justify-content: center;
align-items: center;
}
.effect-active {
background-color: rgba(43, 101, 251, 0.2);
border-width: 2rpx;
border-color: #2B65FB;
}
.effect-icon-container {
width: 112rpx;
height: 112rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16rpx;
background-color: rgba(255, 255, 255, 0.1);
margin-bottom: 12rpx;
}
.effect-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
}
.effect-name {
font-size: 24rpx;
color: #FFFFFF;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<input class="input-wrapper" v-model="inputValue" placeholder="说点什么…" confirm-type="send" @confirm="sendMessage">
</input>
</template>
<script setup lang="ts">
import {
ref, watch
} from "vue";
import {
useBarrageState
} from "@/uni_modules/tuikit-atomic-x/state/BarrageState";
import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
const {
sendTextMessage,
} = useBarrageState(uni?.$liveID);
const { audienceList } = useLiveAudienceState(uni?.$liveID);
const inputValue = ref("");
const isDisableSendMessage = ref(false);
const sendMessage = (event : any) => {
if (isDisableSendMessage.value) {
uni.showToast({ title: '当前直播间内,您已被禁言', icon: 'none' });
inputValue.value = '';
return;
}
const value = event.detail.value;
console.warn('inputValue = ', inputValue, uni?.$liveID);
sendTextMessage({
liveID: uni?.$liveID,
text: value,
success: () => {
console.log('sendTextMessage success');
},
fail: (code, msg) => {
console.error(`sendTextMessage failed, code: ${code}, msg: ${msg}`);
},
})
inputValue.value = ""
};
watch(audienceList, (newValue, oldValue) => {
(newValue || []).forEach((obj, index) => {
if (obj?.userID === uni.$userID) {
const oldUserInfo = (oldValue || [])[index] || {};
if (obj.isMessageDisabled && !oldUserInfo?.isMessageDisabled) {
isDisableSendMessage.value = true;
uni.showToast({
title: '当前房间内\n您已被禁言',
icon: 'none',
duration: 2000,
position: 'center',
});
}
if (!obj.isMessageDisabled && oldUserInfo?.isMessageDisabled) {
isDisableSendMessage.value = false;
uni.showToast({
title: '当前房间内\n您已被解除禁言',
icon: 'none',
duration: 2000,
position: 'center',
});
}
}
});
}, { immediate: true, deep: true });
</script>
<style>
.input-wrapper {
position: relative;
background: rgba(15, 16, 20, 0.4);
border-radius: 50%;
display: flex;
flex-direction: row;
align-items: center;
height: 72rpx;
padding-left: 16rpx;
color: #ffffff;
width: 260rpx;
font-size: 28rpx;
border: 1px solid rgba(255, 255, 255, 0.14);
}
</style>

View File

@@ -0,0 +1,418 @@
<template>
<view class="barrage-container">
<!-- 聊天消息列表 -->
<list class="chat-list" scroll-y :show-scrollbar="false" :style="{ bottom: bottomPx + 'px' }">
<cell class="chat-item" v-if="mixMessageList.length > 0" v-for="(message) in mixMessageList"
:key="message?.sequence" @tap="onItemTap(message)">
<view class="message-content-wrapper">
<view class="nickname-content-gift" v-if="message?.gift">
<text class="chat-nickname"
numberOfLines="1">{{ message?.sender?.userName || message?.sender?.userID }}</text>
<view class="gift-row">
<text class="gift-prefix">{{ giftPrefix }}</text>
<text class="gift-recipient">{{ giftReceiverName }}</text>
<text class="gift-name">{{ message?.textContent || '' }}</text>
<image class="gift-icon" :src="message?.gift?.iconURL || ''" alt="" />
<text class="gift-count" style="font-size: 26rpx;">x{{ message?.count || '1' }}</text>
</view>
</view>
<view class="nickname-content-wrapper" v-else>
<view class="message-right" v-if="message?.sender?.userID === currentLive?.liveOwner.userID">
<text class="message-role">主播</text>
</view>
<view class="nickname-content-wrapper">
<text class="chat-nickname"
numberOfLines="1">{{ message?.sender?.userName || message?.sender?.userID }}</text>
<text class="chat-content">{{ message?.textContent || '' }}</text>
</view>
</view>
</view>
</cell>
<cell ref="ListBottom" style="height: 50rpx;"></cell>
</list>
<!-- GiftToast 提示 -->
<view class="toast-container" v-for="toast in visibleToasts" :key="toast.id" :style="getToastStyle(toast)">
<view class="toast-content">
<!-- 左侧用户信息 -->
<view class="user-info">
<image class="user-avatar" :src="toast?.avatarURL || defaultAvatarURL" mode="aspectFill" />
<view class="user-details">
<text class="username">{{ toast?.name || '' }}</text>
<text class="action-text">{{ toast?.desc || '' }}</text>
</view>
</view>
<!-- 右侧图标 -->
<view class="icon-container" v-if="toast.iconURL">
<image class="icon" :src="toast?.iconURL || ''" mode="aspectFit" />
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
import { useBarrageState } from "@/uni_modules/tuikit-atomic-x/state/BarrageState";
import { useGiftState } from "@/uni_modules/tuikit-atomic-x/state/GiftState";
import { useLiveListState } from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
const props = defineProps<{
mode ?: 'anchor' | 'audience',
bottomPx ?: number,
liveID ?: string,
toast ?: any, // GiftToast 的 toast 属性
}>();
const mode = computed(() => props.mode || 'audience');
const bottomPx = computed(() => props.bottomPx ?? 0);
const liveID = computed(() => props.liveID || uni?.$liveID);
const { messageList } = useBarrageState(liveID.value);
const { addGiftListener, removeGiftListener } = useGiftState(liveID.value);
const { currentLive } = useLiveListState(liveID.value);
const mixMessageList = ref<any[]>(messageList.value || []);
const dom = uni.requireNativePlugin('dom');
const ListBottom = ref('ListBottom');
const giftPrefix = computed(() => mode.value === 'anchor' ? '送给我' : '送给');
const giftReceiverName = computed(() => {
if (mode.value === 'anchor') return '';
const owner = currentLive?.value?.liveOwner || {};
return owner?.userName || owner?.userID || '';
});
// GiftToast 相关状态和逻辑
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const defaultPosition = {
top: 'auto',
bottom: 720,
left: 32,
right: 'auto',
};
const toastHeight = 105;
const visibleToasts = ref<any[]>([]);
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
const showToast = (toastConfig : Partial<any>) => {
const toast = {
id: generateId(),
duration: 3000,
autoHide: true,
position: {
top: defaultPosition.top,
bottom: defaultPosition.bottom + (visibleToasts.value || []).length * toastHeight,
left: '32rpx',
right: defaultPosition.bottom,
},
...toastConfig
};
visibleToasts.value.push(toast);
if (toast.autoHide && toast.duration) {
setTimeout(() => {
hideToast(toast.id);
}, toast.duration);
}
return toast.id;
};
const hideToast = (id : string) => {
const index = visibleToasts.value.findIndex(toast => toast.id === id);
if (index > -1) {
const removedToast = visibleToasts.value.splice(index, 1)[0];
emit('toastClosed', removedToast);
}
};
const hideAllToasts = () => {
visibleToasts.value = [];
};
const getToastStyle = (toast : any) => {
const style : any = {
position: 'fixed',
zIndex: 999
};
if (toast.position) {
if (toast.position.top !== undefined) {
style.top = typeof toast.position.top === 'number' ? `${toast.position.top}rpx` : toast.position.top;
}
if (toast.position.bottom !== undefined) {
style.bottom = typeof toast.position.bottom === 'number' ? `${toast.position.bottom}rpx` : toast.position.bottom;
}
if (toast.position.left !== undefined) {
style.left = typeof toast.position.left === 'number' ? `${toast.position.left}rpx` : toast.position.left;
}
if (toast.position.right !== undefined) {
style.right = typeof toast.position.right === 'number' ? `${toast.position.right}rpx` : toast.position.right;
}
}
return style;
};
watch(messageList, (newVal, oldVal) => {
if (!newVal) return;
const value = newVal.slice((oldVal || []).length, (newVal || []).length);
if (value.length > 0) {
mixMessageList.value = [...mixMessageList.value, ...value];
dom.scrollToElement(ListBottom.value);
}
});
const emit = defineEmits(['itemTap', 'toastClosed']);
const onItemTap = (message : any) => {
emit('itemTap', message);
};
const handleReceiveGift = {
callback: (event) => {
const res = JSON.parse(event)
const value = {
...res,
textContent: `${res.gift?.name || ''}`,
};
mixMessageList.value = [...mixMessageList.value, value];
dom.scrollToElement(ListBottom.value);
}
}
onMounted(() => {
if (props.toast && Object.keys(props.toast).length > 0) {
showToast(props.toast);
}
addGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
});
onUnmounted(() => {
removeGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
});
// 暴露给父组件的方法
defineExpose({
showToast,
hideToast,
hideAllToasts
});
</script>
<style>
.barrage-container {
position: relative;
}
.chat-list {
position: fixed;
left: 32rpx;
right: 32rpx;
bottom: 800rpx;
height: 380rpx;
width: 500rpx;
}
/* GiftToast 样式 */
.toast-container {
/* 基础样式由getToastStyle动态设置 */
}
.toast-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: rgba(34, 38, 46, 0.4);
padding: 20rpx 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.1);
height: 100rpx;
width: 320rpx;
border-radius: 50rpx;
backdrop-filter: blur(10rpx);
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
border-width: 2rpx;
border-color: #8B5CF6;
margin-right: 16rpx;
}
.user-details {
display: flex;
flex-direction: column;
flex: 1;
}
.username {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 4rpx;
max-width: 120rpx;
lines: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.action-text {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
font-weight: 400;
max-width: 120rpx;
lines: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.icon-container {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
width: 80rpx;
height: 80rpx;
}
.chat-item {
flex-direction: row;
align-items: flex-start;
border-radius: 32rpx;
padding: 6rpx;
width: fit-content;
max-width: 500rpx;
}
.message-content-wrapper {
flex-direction: row;
align-items: flex-start;
min-width: 0;
max-width: 500rpx;
width: fit-content;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 999rpx;
padding: 8rpx 12rpx;
}
.nickname-content-wrapper {
flex: 1;
flex-direction: row;
align-items: flex-start;
justify-content: center;
min-width: 0;
}
.nickname-content-gift {
flex-direction: row;
align-items: flex-start;
min-width: 0;
max-width: 500rpx;
width: fit-content;
}
.message-role {
background-color: #0468FC;
border-radius: 999px;
margin-right: 5rpx;
color: #fff;
padding: 5rpx 15rpx;
font-size: 20rpx;
}
.chat-nickname {
color: #80BEF6;
font-size: 24rpx;
line-height: 24rpx;
margin-right: 8rpx;
padding: 5rpx 0;
flex-shrink: 0;
max-width: 200rpx;
lines: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.gift-row {
display: flex;
flex-direction: row;
align-items: center;
max-width: 300rpx;
flex: 1;
justify-content: flex-start;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
flex-wrap: wrap;
padding-top: 2rpx;
}
.gift-icon {
width: 24rpx;
height: 24rpx;
flex-shrink: 0;
margin: 0;
}
.gift-recipient,
.gift-name {
color: #ffffff;
font-size: 24rpx;
line-height: 24rpx;
font-weight: 500;
z-index: 999;
padding: 2rpx 0;
max-width: 300rpx;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
text-align: left;
min-width: 0;
}
.gift-prefix,
.gift-count {
color: #ffffff;
font-size: 24rpx;
line-height: 24rpx;
font-weight: 500;
z-index: 999;
}
.chat-content {
color: #ffffff;
font-size: 24rpx;
line-height: 24rpx;
font-weight: 500;
z-index: 999;
padding: 5rpx 0;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
flex: 1;
min-width: 0;
text-indent: 0;
padding-left: 0;
}
</style>

View File

@@ -0,0 +1,557 @@
<template>
<!-- 美颜面板组件 -->
<!-- 使用方式: <BeautyPanel v-model="showBeautyPanel" @adjust-beauty="handleAdjustBeauty" @reset="handleReset" /> -->
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }" @click.stop>
<!-- 程度控制区域 -->
<view class="slider-section" v-if="currentOptionName !== '关闭'">
<view class="control-container">
<view class="custom-slider">
<!-- 减号按钮 -->
<view style="width: 40rpx; height: 40rpx;" @tap="decreaseValue">
<text class="btn-text">-</text>
</view>
<!-- 进度条区域 -->
<view class="progress-section">
<!-- 进度条背景 -->
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: (currentRealValue / 100 * 400) + 'rpx' }"
></view>
</view>
<!-- 当前数值显示 - 定位在进度条右侧 -->
<text class="current-value">{{ currentRealValue }}</text>
</view>
<!-- 加号按钮 -->
<view style="width: 40rpx; height: 40rpx;" @tap="increaseValue">
<text class="btn-text">+</text>
</view>
</view>
</view>
</view>
<!-- 功能标签页 -->
<!-- <view class="feature-tabs">
<view
v-for="(tab, index) in featureTabs"
:key="index"
class="tab-item"
:class="{ 'active': activeTabIndex === index }"
@tap="switchTab(index)"
>
<text class="tab-text" :class="{ 'active-text': activeTabIndex === index }">{{ tab.name }}</text>
<view v-if="tab.icon" class="tab-icon">
<image :src="tab.icon" mode="aspectFit" class="icon-image" />
</view>
</view>
</view> -->
<!-- 详细选项区域 -->
<view class="options-section">
<view class="options-grid">
<view
v-for="(option, index) in currentOptions"
:key="index"
class="option-item"
:class="{ 'selected': selectedOptionIndex === index }"
@tap="selectOption(index)"
>
<view class="option-icon-container">
<image :src="option.icon" mode="aspectFit" class="option-icon" />
</view>
<text class="option-name">{{ option.name }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState'
const {
setSmoothLevel,
setWhitenessLevel,
setRuddyLevel,
whitenessLevel,
ruddyLevel,
smoothLevel,
realUiValues,
setRealUiValue,
getRealUiValue,
resetRealUiValues
} = useBaseBeautyState(uni.$liveID)
/**
* 美颜面板组件
*
* Props:
* - modelValue: Boolean - 控制面板显示/隐藏
*
* Events:
* - update:modelValue - 更新面板显示状态
* - adjust-beauty - 调整美颜参数事件
* - reset - 重置事件
*/
// Props 定义
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
// Emits 定义
const emit = defineEmits(['update:modelValue', 'adjust-beauty', 'reset'])
// 响应式数据
const activeTabIndex = ref(0)
const selectedOptionIndex = ref(0) // 默认选中"关闭"
const featureTabs = ref([
{ name: '美颜', icon: null },
// { name: '美体', icon: null },
// { name: '滤镜', icon: null },
// { name: '贴纸', icon: null },
// { name: '风格整妆', icon: null },
// { name: '重置', icon: '/static/images/edit.png' }
])
const beautyOptions = ref([
{ name: '关闭', icon: '/static/images/beauty-close.png' },
{ name: '美白', icon: '/static/images/whiteness.png' },
{ name: '磨皮', icon: '/static/images/smooth.png' },
{ name: '红润', icon: '/static/images/live-ruddy.png' },
// { name: '对比度', icon: '/static/images/setting.png' },
// { name: '饱和', icon: '/static/images/setting.png' }
])
const bodyOptions = ref([
{ name: '瘦脸', icon: '/static/images/beauty.png' },
{ name: '大眼', icon: '/static/images/beauty.png' },
{ name: '瘦身', icon: '/static/images/beauty.png' },
{ name: '长腿', icon: '/static/images/beauty.png' }
])
const filterOptions = ref([
{ name: '原图', icon: '/static/images/beauty.png' },
{ name: '暖色', icon: '/static/images/beauty.png' },
{ name: '冷色', icon: '/static/images/beauty.png' },
{ name: '黑白', icon: '/static/images/beauty.png' }
])
const stickerOptions = ref([
{ name: '无', icon: '/static/images/close.png' },
{ name: '帽子', icon: '/static/images/gift.png' },
{ name: '眼镜', icon: '/static/images/gift.png' },
{ name: '胡子', icon: '/static/images/gift.png' }
])
const styleOptions = ref([
{ name: '自然', icon: '/static/images/beauty.png' },
{ name: '清新', icon: '/static/images/beauty.png' },
{ name: '复古', icon: '/static/images/beauty.png' },
{ name: '时尚', icon: '/static/images/beauty.png' }
])
// 计算属性
const currentOptions = computed(() => {
const optionsMap = {
0: beautyOptions.value,
1: bodyOptions.value,
2: filterOptions.value,
3: stickerOptions.value,
4: styleOptions.value
}
return optionsMap[activeTabIndex.value] || beautyOptions.value
})
// 获取当前选项名称
const currentOptionName = computed(() => {
return currentOptions.value[selectedOptionIndex.value]?.name || '关闭'
})
// 获取当前激活的美颜效果名称(排除"关闭"选项)
const activeBeautyOption = computed(() => {
const options = currentOptions.value
for (let i = 0; i < options.length; i++) {
const option = options[i]
if (option.name !== '关闭') {
// 检查这个美颜效果是否有值
let hasValue = false
switch (option.name) {
case '美白':
hasValue = getRealUiValue('whiteness') > 0
break
case '红润':
hasValue = getRealUiValue('ruddy') > 0
break
case '磨皮':
hasValue = getRealUiValue('smooth') > 0
break
}
if (hasValue) {
return option.name
}
}
}
return null
})
// 获取当前选项对应的真实UI显示值显示用户拖动的真实数据
const currentRealValue = computed(() => {
switch (currentOptionName.value) {
case '美白':
return getRealUiValue('whiteness')
case '红润':
return getRealUiValue('ruddy')
case '磨皮':
return getRealUiValue('smooth')
case '关闭':
default:
return 0
}
})
// 监听外部状态变化只在初始化时设置真实的UI值
watch([smoothLevel, whitenessLevel, ruddyLevel], () => {
// 只在组件初始化时如果真实UI值未设置过才从API状态初始化
// 这样可以保持用户拖动的真实数据不被覆盖
if (getRealUiValue('whiteness') === 0 && whitenessLevel.value > 0) {
setRealUiValue('whiteness', Math.round(whitenessLevel.value * 10))
}
if (getRealUiValue('ruddy') === 0 && ruddyLevel.value > 0) {
setRealUiValue('ruddy', Math.round(ruddyLevel.value * 10))
}
if (getRealUiValue('smooth') === 0 && smoothLevel.value > 0) {
setRealUiValue('smooth', Math.round(smoothLevel.value * 10))
}
}, { immediate: true })
// 监听面板显示状态当面板打开时初始化UI值
watch(() => props.modelValue, (newValue) => {
if (newValue) {
// 面板打开时只有在真实UI值未设置过的情况下才从API状态初始化
// 这样可以保持用户上次拖动的真实数据
if (getRealUiValue('whiteness') === 0 && whitenessLevel.value > 0) {
setRealUiValue('whiteness', Math.round((whitenessLevel.value || 0) * 10))
}
if (getRealUiValue('ruddy') === 0 && ruddyLevel.value > 0) {
setRealUiValue('ruddy', Math.round((ruddyLevel.value || 0) * 10))
}
if (getRealUiValue('smooth') === 0 && smoothLevel.value > 0) {
setRealUiValue('smooth', Math.round((smoothLevel.value || 0) * 10))
}
}
})
// 方法
/**
* 关闭面板
*/
const close = () => {
emit('update:modelValue', false)
}
/**
* 减少数值
*/
const decreaseValue = () => {
const newValue = Math.max(0, currentRealValue.value - 10)
updateBeautyValue(newValue)
}
/**
* 增加数值
*/
const increaseValue = () => {
const newValue = Math.min(100, currentRealValue.value + 10)
updateBeautyValue(newValue)
}
/**
* 更新美颜数值
*/
const updateBeautyValue = (uiValue) => {
const currentOption = currentOptions.value[selectedOptionIndex.value]
// 保存真实的UI拖动值到全局状态
switch (currentOption.name) {
case '美白':
setRealUiValue('whiteness', uiValue)
break
case '红润':
setRealUiValue('ruddy', uiValue)
break
case '磨皮':
setRealUiValue('smooth', uiValue)
break
case '关闭':
return
}
// 如果UI值超过90API调用时限制为90
const limitedUiValue = uiValue > 90 ? 90 : uiValue
const apiValue = limitedUiValue / 10 // 转换为接口值0-9
// 调用对应的设置方法,传入转换后的参数值
switch (currentOption.name) {
case '美白':
setWhitenessLevel({ whitenessLevel: apiValue })
break
case '红润':
setRuddyLevel({ ruddyLevel: apiValue })
break
case '磨皮':
setSmoothLevel({ smoothLevel: apiValue })
break
}
emit('adjust-beauty', {
type: featureTabs.value[activeTabIndex.value].name,
option: currentOption.name,
value: uiValue
})
}
/**
* 切换标签页
*/
const switchTab = (index) => {
activeTabIndex.value = index
selectedOptionIndex.value = 0
// 如果是重置标签,触发重置事件
if (index === 5) {
emit('reset')
// 重置所有美颜参数
setSmoothLevel({ smoothLevel: 0 })
setWhitenessLevel({ whitenessLevel: 0 })
setRuddyLevel({ ruddyLevel: 0 })
selectedOptionIndex.value = 0
activeTabIndex.value = 0
}
}
/**
* 选择选项
*/
const selectOption = (index) => {
const currentOption = currentOptions.value[index]
// 如果选择"关闭",重置当前选中的美颜效果
if (currentOption.name === '关闭') {
// 获取当前选中的美颜效果在更新selectedOptionIndex之前
const currentSelectedOption = currentOptions.value[selectedOptionIndex.value]
// 根据当前选中的美颜效果来重置对应的参数
switch (currentSelectedOption?.name) {
case '美白':
setWhitenessLevel({ whitenessLevel: 0 })
setRealUiValue('whiteness', 0)
break
case '红润':
setRuddyLevel({ ruddyLevel: 0 })
setRealUiValue('ruddy', 0)
break
case '磨皮':
setSmoothLevel({ smoothLevel: 0 })
setRealUiValue('smooth', 0)
break
default:
// 如果当前选中的不是具体的美颜选项,则重置所有
setSmoothLevel({ smoothLevel: 0 })
setWhitenessLevel({ whitenessLevel: 0 })
setRuddyLevel({ ruddyLevel: 0 })
resetRealUiValues()
break
}
}
selectedOptionIndex.value = index
emit('adjust-beauty', {
type: featureTabs.value[activeTabIndex.value].name,
option: currentOption.name,
value: currentRealValue.value // 使用UI显示值0-100
})
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
}
.drawer-open {
transform: translateY(0);
}
/* 滑块区域 */
.slider-section {
padding: 40rpx 48rpx 20rpx;
background-color: rgba(34, 38, 46, 1);
}
/* 自定义控制区域样式 */
.control-container {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.custom-slider {
flex: 1;
flex-direction: row;
align-items: center;
margin: 0 20rpx;
}
.control-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx solid #2b6ad6;
}
.minus-btn {
display: flex;
justify-self: center;
align-items: center;
background-color: rgba(43, 106, 214, 0.1);
}
.plus-btn {
display: flex;
align-items: center;
justify-self: center;
background-color: #2b6ad6;
}
.btn-text {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
}
.plus-btn .btn-text {
color: #ffffff;
}
.progress-section {
flex: 1;
margin: 0 20rpx;
align-items: center;
flex-direction: row;
justify-content: center;
}
.progress-bar {
width: 400rpx;
height: 8rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4rpx;
position: relative;
overflow: hidden;
margin-right: 16rpx;
}
.progress-fill {
height: 8rpx;
background-color: #2b6ad6;
border-radius: 4rpx;
}
.current-value {
font-size: 24rpx;
color: #ffffff;
font-weight: 600;
text-align: center;
z-index: 10;
}
/* 详细选项区域 */
.options-section {
flex: 1;
background-color: rgba(34, 38, 46, 1);
padding: 20rpx 48rpx;
}
.options-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.option-item {
width: 120rpx;
margin-bottom: 30rpx;
align-items: center;
padding: 16rpx 12rpx;
border-radius: 12rpx;
border: 2rpx solid transparent;
}
.option-item.selected {
border-color: #2b6ad6;
background-color: #2b6ad6;
}
.option-icon-container {
width: 60rpx;
height: 60rpx;
margin-bottom: 12rpx;
justify-content: center;
align-items: center;
}
.option-icon {
width: 60rpx;
height: 60rpx;
}
.option-name {
font-size: 22rpx;
color: #ffffff;
text-align: center;
}
</style>

View File

@@ -0,0 +1,433 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="audience-header">
<view class="tab-container">
<!-- <view
class="tab-item"
:class="activeTab === 'invite' ? 'active' : 'inactive'"
@tap="activeTab = 'invite'"
>
<text :class="activeTab === 'invite' ? 'active-text' : 'inactive-text'">邀请连麦</text>
<view class="active-line-container" v-if="activeTab === 'invite'">
<view class="active-line"></view>
</view>
</view> -->
<view class="tab-item" :class="activeTab === 'requests' ? 'active' : 'inactive'"
@tap="activeTab = 'requests'">
<text :class="activeTab === 'requests' ? 'active-text' : 'inactive-text'">连麦申请</text>
<!-- <view class="active-line-container" v-if="activeTab === 'requests'">
<view class="active-line"></view>
</view> -->
</view>
</view>
</view>
<scroll-view class="audience-content" scroll-y @scroll="handleScroll" :scroll-top="scrollTop">
<!-- 连麦申请列表 -->
<view v-if="activeTab === 'requests'">
<view v-if="(applicants || []).length > 0" class="audience-grid">
<view v-for="audience in applicants || []" :key="audience?.userID" class="audience-item"
:class="{ 'is-admin': audience?.userRole === 'administrator' }">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" :src="audience?.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ audience?.userName || audience?.userID}}</text>
</view>
<view class="request-actions">
<view class="action-btn accept" @tap="handleRequest(audience, 'accept')">
<text class="btn-text">同意</text>
</view>
<view class="action-btn reject" @tap="handleRequest(audience, 'reject')">
<text class="btn-text">拒绝</text>
</view>
</view>
</view>
</view>
<!-- <view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" :src="audience.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ audience.userName }}</text>
</view>
<view
class="start-link"
:class="audience?.isMessageDisabled ? 'start' : 'waiting'"
@tap="startLink(audience)"
>
<text class="link-text">{{ audience?.isMessageDisabled ? '发起连线' : '邀请中(30)s' }}</text>
</view>
</view>
</view>-->
<view class="audience-item-bottom-line"></view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无观众申请连麦</text>
<view></view>
</view>
</view>
<!-- 邀请连麦列表 -->
<!-- <view v-if="activeTab === 'invite'">
<view v-if="(invitableGuests || []).length > 0" class="audience-grid">
<view v-for="request in invitableGuests || []" :key="request.userID" class="audience-item">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar"
src="https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/trtc/live/assets/Icon/defaultAvatar.png"
mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ request.userName || request.userID}}</text>
</view>
<view class="request-actions">
<view class="action-btn accept" @tap="handleRequest(request, 'accept')">
<text class="btn-text">同意</text>
</view>
<view class="action-btn reject" @tap="handleRequest(request, 'reject')">
<text class="btn-text">拒绝</text>
</view>
</view>
</view>
</view>
<view class="audience-item-bottom-line"></view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无邀请连麦</text>
</view> -->
<!-- </view> -->
<view v-if="isLoading" class="loading-state">
<image src="/static/images/more.png" mode="aspectFit" class="loading-image" />
</view>
</scroll-view>
<!-- <view class="drawer-content">
<view class="divider-line-container">
<view class="divider-line"></view>
</view>
</view> -->
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue'
import {
useCoGuestState
} from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
const {
applicants,
invitableGuests,
acceptApplication,
cancelApplication,
rejectApplication
} = useCoGuestState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
activeTab: {
type: String,
default: 'invite',
},
})
const isLoading = ref(false);
const currentCursor = ref(0);
const scrollTop = ref(0);
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false);
}
const handleScroll = (e) => {
if (this.currentCursor.value === 0) return;
const {
scrollHeight,
scrollTop
} = e.detail;
scrollTop.value = scrollTop;
if (scrollHeight - scrollTop.value < 100) {
// this.loadAudiences(this.currentCursor);
}
};
const handleRequest = (audience, action) => {
console.warn(`${action} request from ${JSON.stringify(audience)}`);
if (action === 'accept') {
acceptApplication({
liveID: uni?.$liveID,
userID: audience?.userID,
success: () => {
console.log('acceptApplication success.');
},
fail: (errCode, errMsg) => {
console.error(`acceptApplication fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
return;
}
if (action === 'reject') {
rejectApplication({
liveID: uni?.$liveID,
userID: audience?.userID,
success: () => {
console.log('rejectApplication success.');
},
fail: (errCode, errMsg) => {
console.error(`rejectCoGuestRequest fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
uni.$localGuestStatus = 'IDLE'
}
};
const startLink = (audience) => {
console.warn('发起连线');
};
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
height: 1000rpx;
}
.drawer-open {
transform: translateY(0);
}
.audience-header {
padding: 40rpx 0;
background-color: rgba(34, 38, 46, 1);
position: relative;
z-index: 1;
flex-direction: column;
align-items: center;
}
.tab-container {
flex-direction: row;
justify-content: center;
margin-top: 20rpx;
}
.tab-item {
padding: 20rpx 40rpx;
margin: 0 20rpx;
border-radius: 8rpx;
}
.tab-text {
font-size: 32rpx;
}
.active-text {
color: rgba(255, 255, 255, 0.9);
}
.active-line-container {
margin-top: 4rpx;
display: flex;
justify-content: center;
align-items: center;
width: 128rpx;
}
.active-line {
border-bottom: 4rpx solid rgba(255, 255, 255, 0.9);
width: 80rpx;
}
.inactive-text {
color: rgba(255, 255, 255, 0.3);
}
.audience-content {
flex: 1;
padding: 0 48rpx;
}
.audience-grid {
flex-direction: column;
}
.audience-item {
padding: 24rpx 0;
position: relative;
}
.audience-info {
flex-direction: row;
align-items: center;
}
.audience-avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
}
.audience-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.audience-item-right {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.audience-name {
font-size: 32rpx;
color: #ffffff;
max-width: 200rpx;
lines: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.start-link {
padding: 12rpx 32rpx;
border-radius: 100rpx;
background-color: rgba(43, 106, 214, 1);
}
.start-link.waiting {
background-color: rgba(58, 60, 66, 1);
}
.link-text {
color: #ffffff;
font-size: 28rpx;
}
.request-actions {
flex-direction: row;
}
.action-btn {
padding: 12rpx 32rpx;
border-radius: 100rpx;
margin-left: 16rpx;
}
.action-btn.accept {
background-color: rgba(43, 106, 214, 1);
}
.action-btn.reject {
background-color: rgba(58, 60, 66, 1);
}
.btn-text {
color: #ffffff;
font-size: 28rpx;
}
.audience-item-bottom-line {
position: absolute;
border-bottom-width: 1rpx;
border-bottom-color: rgba(79, 88, 107, 0.3);
width: 550rpx;
height: 2rpx;
bottom: 0;
right: 0;
}
.empty-state,
.loading-state {
padding: 64rpx;
justify-content: center;
align-items: center;
}
.empty-text {
color: #999999;
font-size: 28rpx;
}
.loading-image {
width: 48rpx;
height: 48rpx;
}
.drawer-content {
padding: 24rpx 0;
}
.divider-line-container {
height: 68rpx;
justify-content: center;
position: relative;
}
.divider-line {
width: 268rpx;
height: 10rpx;
border-radius: 200rpx;
background-color: #ffffff;
position: absolute;
bottom: 16rpx;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="drawer-header">
<text style="color: rgba(213, 224, 242, 1); font-size: 32rpx;">选择连麦方式</text>
<text
style="font-size: 24rpx; font-weight: 400; color: rgba(124, 133, 166, 1); margin-top: 20rpx;">选择连麦方式,主播同意后接通</text>
</view>
<view class="drawer-content">
<view class="drawer-actions">
<view style="height: 2rpx; color: rgba(79, 88, 107, 0.3); width: 750rpx; background: #fff; opacity: 0.2;">
</view>
<view style="display: flex; flex-direction: row; align-items: center; padding: 30rpx;"
@click="handleSendCoGuest('video')">
<image src="/static/images/mode.png" style="width: 36rpx; height: 36rpx;"></image>
<text
style="font-size: 32rpx; font-weight: 400; color: rgba(213, 224, 242, 1); padding-left: 10rpx;">申请视频连麦</text>
</view>
<view style="height: 2rpx; color: rgba(79, 88, 107, 0.3); width: 750rpx; background: #fff; opacity: 0.2;">
</view>
<view style="display: flex; flex-direction: row; align-items: center; padding: 30rpx;"
@click="handleSendCoGuest('mic')">
<image src="/static/images/live-comic.png" style="width: 36rpx; height: 36rpx;"></image>
<text
style="font-size: 32rpx; font-weight: 400; color: rgba(213, 224, 242, 1); padding-left: 10rpx;">申请语音连麦</text>
</view>
</view>
<!-- <view class="divider-line-container">
<view class="divider-line"></view>
</view> -->
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { watch, onMounted, ref, onUnmounted } from 'vue';
import { useDeviceState } from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
import { useCoGuestState } from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
const { loginUserInfo } = useLoginState()
const currentCoGuestType = ref('')
const {
// 响应式状态 - 麦克风相关
microphoneStatus, microphoneStatusReason, microphoneLastError, hasPublishAudioPermission, captureVolume,
// 响应式状态 - 摄像头相关
cameraStatus, cameraStatusReason, cameraLastError,
// 响应式状态 - 其他设备相关
currentAudioRoute, isScreenSharing, isFrontCamera, screenStatus, screenStatusReason,
// 操作方法 - 麦克风相关callback在params中
openLocalMicrophone, closeLocalMicrophone, muteLocalAudio, unmuteLocalAudio, setAudioRoute,
// 操作方法 - 摄像头相关callback在params中
openLocalCamera, closeLocalCamera, switchCamera, switchMirror, updateVideoQuality,
// 操作方法 - 屏幕共享相关callback在params中
startScreenShare, stopScreenShare,
} = useDeviceState(uni?.$liveID);
const {
// 响应式状态
invitees, applicants,
applyForSeat,
addCoGuestHostListener, removeCoGuestHostListener,
addCoGuestGuestListener, removeCoGuestGuestListener
} = useCoGuestState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
liveID: {
type: String,
},
userID: {
type: String,
default: '',
},
seatIndex: {
type: Number,
default: -1,
},
});
const emit = defineEmits(['update:modelValue']);
const close = () => {
emit('update:modelValue', false);
};
onMounted(() => {
addCoGuestGuestListener(uni.$liveID, 'onGuestApplicationResponded', handleGuestApplicationResponded)
addCoGuestGuestListener(uni.$liveID, 'onGuestApplicationNoResponse', handleGuestApplicationNoResponse)
})
onUnmounted(() => {
currentCoGuestType.value = ''
removeCoGuestGuestListener(uni.$liveID, 'onGuestApplicationResponded', handleGuestApplicationResponded)
removeCoGuestGuestListener(uni.$liveID, 'onGuestApplicationNoResponse', handleGuestApplicationNoResponse)
})
const handleGuestApplicationResponded = {
callback: (event) => {
const res = JSON.parse(event)
if (res.isAccept) {
uni.$localGuestStatus = 'CONNECTED'
if (currentCoGuestType.value === 'video') {
openLocalCamera({
isFront: true,
success: () => {
console.log('openLocalCamera success.');
},
fail: (errCode, errMsg) => {
console.error(`openLocalCamera fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
}
openLocalMicrophone({
success: () => {
console.log('openLocalMicrophone success.');
},
fail: (errCode, errMsg) => {
console.error(`openLocalMicrophone fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
} else {
uni.$localGuestStatus = 'IDLE'
uni.showToast({
title: '上麦申请被拒绝',
icon: 'none',
duration: 2000,
});
}
}
}
const handleGuestApplicationNoResponse = {
callback: (event) => {
const res = JSON.parse(event)
if (res.reason === 'TIMEOUT') {
uni.$localGuestStatus = 'IDLE'
uni.showToast({
title: '上麦申请超时',
icon: 'none',
duration: 2000,
});
}
}
}
const handleSendCoGuest = (type : string) => {
console.log(`goGuest localStatus: ${uni.$localGuestStatus}`);
currentCoGuestType.value = type
if (uni.$localGuestStatus === 'USER_APPLYING') {
console.log(`cancel userID: ${props?.userID}`);
uni.showToast({
title: '你已提交了连麦申请 \n请勿重复申请',
icon: 'none',
duration: 2000,
position: 'center',
});
close();
return
}
if (uni.$localGuestStatus === 'IDLE') {
uni.showToast({
title: '你提交了连麦申请 \n请等待主播同意',
icon: 'none',
duration: 2000,
position: 'center',
});
applyForSeat({
liveID: props.liveID,
seatIndex: props.seatIndex, // 申请上麦传 -1, 随机分配麦位, xinlxin 反馈
timeout: 30,
});
uni.$localGuestStatus = 'USER_APPLYING'
close();
return
}
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
height: 500rpx;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
padding: 48rpx;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.drawer-header-title {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
}
.drawer-content {
flex: 1;
}
.drawer-actions {
display: flex;
flex-direction: column;
/* justify-content: space-around; */
/* justify-content: flex-start; */
}
.action-btn {
flex-direction: column;
align-items: center;
margin-right: 40rpx;
flex: 1;
height: 150rpx;
}
.action-btn-image-container {
width: 100rpx;
height: 100rpx;
background-color: rgba(43, 44, 48, 1);
margin-bottom: 12rpx;
border-radius: 25rpx;
justify-content: center;
align-items: center;
}
.action-btn-image {
width: 50rpx;
height: 50rpx;
}
.action-btn-content {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.divider-line-container {
height: 68rpx;
justify-content: center;
position: relative;
}
.divider-line {
width: 268rpx;
height: 10rpx;
border-radius: 200rpx;
background-color: #ffffff;
position: absolute;
bottom: 16rpx;
}
.camera-mic-setting {
flex: 1;
background-color: #1f1024;
}
</style>

View File

@@ -0,0 +1,581 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }" @click.stop>
<view class="audience-header">
<view class="tab-container">
<view class="tab-item">
<text class="active-text">发起连线</text>
<view class="end-right" @tap.stop="handleExitCoHost" v-if="connected?.length > 0">
<image class="end-connect" src="/static/images/logout.png"></image>
<text style="color: #E6594C; padding-left: 10rpx;">断开</text>
</view>
</view>
</view>
</view>
<view v-if="filteredConnected?.length > 0"
style="display: flex; width: 800rpx; padding-left: 80rpx; margin-bottom: 40rpx;">
<text class="title-text">连线中</text>
<view v-for="connect in filteredConnected" :key="connect.userID" class="audience-item">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" mode="aspectFill" :src="connect.avatarURL || defaultAvatarURL" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ connect.userName || connect.userID}}</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="liveList?.length > 0" class="audience-grid">
</view>
<view
style="display: flex; width: 800rpx; flex-direction: row; padding-left: 80rpx; padding-right: 80rpx;margin-bottom: 40rpx; justify-content:space-between; "
@click.stop>
<text class="title-text">推荐列表</text>
<text class="title-text" @click="handleRefresh">刷新</text>
</view>
<list class="audience-content" @loadmore="loadMore" :show-scrollbar="false">
<cell v-for="host in currentInviteHosts" :key="host?.liveOwner?.userID">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" mode="aspectFill" :src="host?.liveOwner?.avatarURL || defaultAvatarURL" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name"
:numberOfLines="1">{{ host?.liveOwner?.userName || host?.liveOwner?.userID}}</text>
</view>
<view :class=" isHostInviting(host) ? 'start-link-waiting' : 'start-link' " @tap="onStartLinkTap(host)">
<text class="start-link-text">{{ isHostInviting(host) ? '邀请中' : '邀请连线' }}</text>
</view>
</view>
</view>
<view class="audience-item-bottom-line"></view>
</cell>
</list>
</view>
</view>
</template>
<script setup lang="ts">
import {
ref,
reactive,
onMounted,
computed,
watch,
nextTick
} from 'vue'
import {
useCoHostState
} from "@/uni_modules/tuikit-atomic-x/state/CoHostState"
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
import {
useLoginState
} from "@/uni_modules/tuikit-atomic-x/state/LoginState";
import { useCoGuestState } from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
const { applicants, rejectApplication } = useCoGuestState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const refreshing = ref('hide')
const isLoading = ref(false)
const scrollTop = ref(0)
const activeTab = ref('pk')
const showDrawer = ref(false)
const selectedAudience = ref(null)
const screenWidth = ref(350)
const {
candidates,
connected,
requestHostConnection,
invitees,
exitHostConnection,
} = useCoHostState(uni?.$liveID)
const {
// 响应式状态
liveListCursor,
currentLive,
// 操作方法 - callback在params中
fetchLiveList,
liveList
} = useLiveListState();
const {
loginUserInfo
} = useLoginState()
const currentCursor = ref('')
const currentUserID = ref(loginUserInfo.value?.userID);
const filteredConnected = computed(() => {
const list = connected?.value || [];
const selfId = uni?.$userID ?? currentUserID.value;
return list.filter(item => item?.userID !== selfId);
})
function filterInviteHosts(list) {
const hosts = Array.isArray(list) ? list : [];
const connectedLiveIds = new Set((connected?.value || []).map(item => item?.liveID));
return hosts.filter(item =>
item?.liveOwner?.userID !== currentLive?.value?.liveOwner?.userID &&
!connectedLiveIds.has(item?.liveID)
);
}
const currentInviteHosts = ref(filterInviteHosts(liveList?.value))
const isRefreshEnd = ref(false)
watch(() => loginUserInfo.value?.userID, (newUserId, oldUserId) => {
console.log('用户ID变化:', {
newUserId
});
if (newUserId) {
// 如果当前标题是默认值或者为空,则更新为新的用户名
currentUserID.value = newUserId;
}
}, {
immediate: true,
deep: true
});
watch(liveList, (newHostsList, olderHostList) => {
if (newHostsList) {
currentInviteHosts.value = filterInviteHosts(newHostsList)
}
nextTick()
}, {
immediate: true,
deep: true
},
)
watch(currentLive, () => {
currentInviteHosts.value = filterInviteHosts(liveList?.value)
}, {
immediate: true,
deep: true
})
watch(applicants, (newVal, oldVal) => {
if (newVal && invitees?.value.length > 0) {
newVal.forEach(applicant => {
rejectApplication({
liveID: uni?.$liveID,
userID: applicant.userID,
});
});
}
}, {
immediate: true,
deep: true
},)
watch(liveListCursor, (newCursor, oldCursor) => {
isRefreshEnd.value = newCursor === ''
})
// 监听连线状态变化,重新过滤推荐列表
watch(connected, (newConnected, oldConnected) => {
currentInviteHosts.value = filterInviteHosts(liveList?.value);
}, {
immediate: true,
deep: true
})
const handleRefresh = () => {
const params = {
cursor: '', // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20, // 分页拉取的个数
success: () => {
fetchLiveListRecursively(liveListCursor.value); // 最多尝试3次
}
};
fetchLiveList(params);
}
const fetchLiveListRecursively = (cursor) => {
const params = {
cursor: cursor,
count: 20,
success: () => {
if (liveListCursor.value) {
fetchLiveListRecursively(liveListCursor.value);
} else {
uni.showToast({
title: '刷新完成'
});
}
},
fail: (err) => {
console.error(`fetchLiveListRecursively failed, err: ${JSON.stringify(err)}`);
}
};
fetchLiveList(params);
}
const loadMore = () => {
if (!liveListCursor.value) {
uni.showToast({
title: "没有更多了",
icon: "none"
});
return;
}
const params = {
cursor: liveListCursor.value,
count: 20,
};
fetchLiveList(params);
};
// 判断当前 host 是否处于邀请中(依据 invitees 列表里的 liveID
function isHostInviting(host) {
const inviteesList = invitees?.value;
const targetLiveID = host?.liveID;
if (!targetLiveID) return false;
return inviteesList.some((item) => item?.liveID === targetLiveID);
}
// 点击邀请,若已在邀请中则不重复发起
function onStartLinkTap(host) {
if (isHostInviting(host)) {
return;
}
startLink(host);
}
function close() {
emit('update:modelValue', false)
}
function startLink(host) {
if (applicants.value.length > 0) {
uni.showToast({
title: '有人申请连麦,无法发起连线',
icon: 'none'
})
return
}
requestHostConnection({
liveID: uni?.$liveID,
targetHostLiveID: host.liveID,
layoutTemplate: 'HOST_DYNAMIC_GRID',
timeout: 30,
extensionInfo: "",
success: (res) => {
console.log(res)
},
fail: (errorCode) => {
console.log(errorCode)
if (errorCode === 5) {
uni.showToast({
icon: 'none',
title: '主播连线中,无法发起连线'
})
} else {
uni.showToast({
icon: 'none',
title: '连线失败'
})
}
}
},)
}
const handleExitCoHost = () => {
uni.showModal({
content: '确定要退出连线吗?',
confirmText: '退出连线',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
exitHostConnection({
liveID: uni?.$liveID,
success: () => {
close()
},
fail: (error) => {
console.log(error)
}
})
}
}
})
}
</script>
<style>
.refresh-icon-container {
margin-left: 16rpx;
width: 40rpx;
height: 40rpx;
justify-content: center;
align-items: center;
border-radius: 8rpx;
background-color: #ffffff;
}
.refresh-icon {
width: 32rpx;
height: 32rpx;
display: flex;
justify-content: center;
align-items: center;
}
.refresh-image {
width: 32rpx;
height: 32rpx;
}
.refresh-icon.refreshing {
animation: rotate 1s linear infinite;
}
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
display: flex;
flex-direction: column;
align-items: center;
height: 1000rpx;
}
.drawer-open {
transform: translateY(0);
}
.audience-header {
padding: 20rpx 0;
background-color: rgba(34, 38, 46, 1);
z-index: 1;
flex-direction: column;
align-items: center;
}
.tab-container {
flex-direction: row;
justify-content: center;
margin-top: 20rpx;
}
.tab-item {
padding: 20rpx 40rpx;
margin: 0 20rpx;
border-radius: 8rpx;
display: flex;
justify-content: center;
flex-direction: row;
width: 750rpx;
flex: 1;
}
.end-right {
position: absolute;
right: 80rpx;
left: auto;
display: flex;
flex-direction: row;
}
.end-connect {
width: 40rpx;
height: 40rpx;
}
.tab-text {
font-size: 32rpx;
}
.active-text {
color: rgba(255, 255, 255, 0.9);
}
.title-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
}
.active-line-container {
margin-top: 4rpx;
display: flex;
justify-content: center;
align-items: center;
width: 128rpx;
}
.active-line {
border-bottom: 4rpx solid rgba(255, 255, 255, 0.9);
width: 80rpx;
}
.inactive-text {
color: rgba(255, 255, 255, 0.3);
}
.audience-return-arrow-container {
position: absolute;
top: 40rpx;
left: 48rpx;
width: 48rpx;
height: 48rpx;
justify-content: center;
align-items: center;
}
.audience-return-arrow {
height: 28rpx;
width: 16rpx;
}
.audience-content {
flex: 1;
width: 750rpx;
padding: 0 48rpx;
background-color: rgba(34, 38, 46, 1);
}
.audience-grid {
flex-direction: column;
}
.audience-item {
border-radius: 16rpx;
height: 100rpx;
color: #ffffff;
position: relative;
margin-bottom: 32rpx;
padding-top: 10rpx;
}
.audience-info {
flex-direction: row;
align-items: center;
padding: 10rpx 0;
}
.audience-avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
overflow: hidden;
}
.audience-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.audience-item-right {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.audience-detail {
flex-direction: row;
}
.audience-name {
font-size: 32rpx;
font-weight: 500;
color: #ffffff;
width: 300rpx;
lines: 1;
}
.start-link {
width: 120rpx;
height: 40rpx;
border-radius: 30rpx;
justify-content: center;
align-items: center;
background-color: rgba(28, 102, 229, 1);
}
.start-link-waiting {
width: 120rpx;
height: 40rpx;
border-radius: 30rpx;
justify-content: center;
align-items: center;
background-color: transparent;
border-width: 2rpx;
border-style: solid;
border-color: rgba(28, 102, 229, 1);
}
.start-link-text {
font-size: 24rpx;
color: #ffffff;
}
.audience-item-bottom-line {
position: absolute;
border-bottom-width: 2rpx;
border-bottom-style: solid;
border-bottom-color: rgba(79, 88, 107, 0.3);
width: 550rpx;
height: 2rpx;
bottom: 0;
right: 0;
}
.empty-state,
.loading-state {
padding: 64rpx;
justify-content: center;
align-items: center;
}
.drawer-content {
padding: 0 48rpx;
height: 68rpx;
}
</style>

View File

@@ -0,0 +1,320 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="gift-header">
<view class="header-content">
<text class="gift-title">礼物</text>
</view>
</view>
<swiper class="gift-content" :current="currentPage" @change="handleSwiperChange" style="height: 710rpx;">
<swiper-item v-for="(page, pageIndex) in giftPages" :key="pageIndex" class="gift-page">
<view class="gift-container">
<view class="gift-item" v-for="(giftInfo, index) in page" :key="giftInfo.giftID"
:class="{ 'selected': selectedGiftIndex === (pageIndex * itemsPerPage + index) }"
@tap="selectGift(pageIndex * itemsPerPage + index)">
<view class="gift-image-container">
<image class="gift-image" :src="giftInfo.iconURL" mode="aspectFit" />
</view>
<view class="gift-action" v-if="selectedGiftIndex === (pageIndex * itemsPerPage + index)">
<view class="send-btn selected-btn" @tap.stop="handleSendGift(pageIndex * itemsPerPage + index)">
<text class="send-text">赠送</text>
</view>
</view>
<text
class="gift-name">{{ selectedGiftIndex === (pageIndex * itemsPerPage + index) ? '' : (giftInfo.name || '') }}</text>
<text class="gift-price">{{ giftInfo.coins }}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
import {
ref
} from 'vue';
import {
downloadAndSaveToPath
} from '@/uni_modules/tuikit-atomic-x/components/GiftPlayer/giftService';
import {
useGiftState
} from "@/uni_modules/tuikit-atomic-x/state/GiftState";
const {
usableGifts,
latestGift,
sendGift,
refreshUsableGifts
} = useGiftState(uni?.$liveID);
export default {
name: 'GiftPanel',
props: {
modelValue: {
type: Boolean,
default: false
},
onGiftSelect: {
type: Function,
default: null
},
},
data() {
return {
scrollTop: 0,
selectedGiftIndex: 0,
giftLists: usableGifts,
currentPage: 0,
itemsPerPage: 8,
};
},
methods: {
close() {
this.$emit('update:modelValue', false);
},
handleSwiperChange(e) {
this.currentPage = e.detail.current;
},
selectGift(index) {
this.selectedGiftIndex = index;
},
handleSendGift(index) {
const gift = (this.flattenedGifts || [])[index];
if (this.selectedGiftIndex !== index) return;
if (this.onGiftSelect) {
this.onGiftSelect(gift);
}
this.selectedGiftIndex = -1;
},
handleRecharge() {
this.$emit('recharge');
}
},
computed: {
// 兼容新的分类结构与旧的扁平结构
flattenedGifts() {
const list = this.giftLists || [];
if (!Array.isArray(list)) return [];
// 新结构:[{ categoryID, name, giftList: [...] }, ...]
if (list.length > 0 && list[0] && Array.isArray(list[0].giftList)) {
const merged = [];
for (let i = 0; i < list.length; i++) {
const category = list[i];
const gifts = Array.isArray(category.giftList) ? category.giftList : [];
for (let j = 0; j < gifts.length; j++) {
merged.push(gifts[j]);
}
}
return merged;
}
// 旧结构:直接为礼物数组
return list;
},
giftPages() {
const pages = [];
const list = this.flattenedGifts;
for (let i = 0; i < list.length; i += this.itemsPerPage) {
pages.push(list.slice(i, i + this.itemsPerPage));
}
return pages;
}
},
watch: {
async giftLists(newVal) {
const flatten = () => {
if (!Array.isArray(newVal)) return [];
if (newVal.length > 0 && newVal[0] && Array.isArray(newVal[0].giftList)) {
// 分类结构
const out = [];
for (let i = 0; i < newVal.length; i++) {
const gifts = Array.isArray(newVal[i].giftList) ? newVal[i].giftList : [];
for (let j = 0; j < gifts.length; j++) out.push(gifts[j]);
}
return out;
}
return newVal;
};
const flatList = flatten();
if (!flatList || flatList.length === 0) return;
for (let i = 0; i < flatList.length; i++) {
const giftData = flatList[i];
if (giftData && giftData.resourceURL) {
const giftKey = `${(giftData.name || '').split(' ').join('')}-${giftData.giftID}`;
if (plus && plus.storage && plus.storage.getAllKeys && plus.storage.getAllKeys().includes(giftKey)) continue;
const svgaGiftSourceUrl = plus && plus.storage ? plus.storage.getItem(giftKey) : null;
if (!svgaGiftSourceUrl) {
const filePath = await downloadAndSaveToPath(`${giftData.resourceURL}`);
if (plus && plus.storage) plus.storage.setItem(giftKey, filePath);
}
}
}
}
},
mounted() {
if (!uni?.$liveID) {
return;
}
refreshUsableGifts({
liveID: uni.$liveID
})
},
};
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
height: 710rpx;
}
.drawer-open {
transform: translateY(0);
}
.gift-header {
padding: 40rpx 48rpx;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
position: relative;
z-index: 1;
}
.header-content {
flex-direction: row;
justify-content: center;
align-items: center;
}
.gift-title {
font-size: 36rpx;
color: #ffffff;
font-weight: 600;
}
.gift-content {
flex: 1;
height: 710rpx;
}
.gift-page {
flex: 1;
height: 710rpx;
}
.gift-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0 20rpx;
flex: 1;
height: 710rpx;
}
.gift-item {
width: 168rpx;
/* 4列布局 */
margin-bottom: 24rpx;
align-items: center;
border-radius: 20rpx;
padding: 10rpx 6rpx 6rpx 6rpx;
border: 2rpx solid transparent;
background-color: transparent;
box-sizing: border-box;
height: 230rpx;
}
.gift-item.selected {
border-color: #2B6AD6;
background-color: rgba(43, 106, 214, 0.12);
height: 230rpx;
}
.gift-image-container {
width: 110rpx;
height: 110rpx;
margin-bottom: 12rpx;
justify-content: center;
align-items: center;
}
.gift-image {
width: 110rpx;
height: 110rpx;
}
.gift-action {
height: 56rpx;
/* 固定高度避免布局抖动 */
margin-bottom: 8rpx;
justify-content: center;
align-items: center;
}
.send-btn {
padding: 8rpx 24rpx;
border-radius: 100rpx;
background-color: rgba(58, 60, 66, 1);
justify-content: center;
align-items: center;
}
.send-btn.selected-btn {
background-color: #2b6ad6;
}
.send-text {
color: #ffffff;
font-size: 24rpx;
}
/* 名称与价格样式 */
.gift-name {
color: #ffffff;
font-size: 26rpx;
line-height: 36rpx;
text-align: center;
max-width: 150rpx;
lines: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.gift-price {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 28rpx;
text-align: center;
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<svga-player ref="playerRef" :url="computedUrl" style="position: absolute; z-index: -1;" @onFinished="handleFinished"
:style="modelValue ? {
top: 100 + 'px',
left: (safeArea?.left || 0) + 'px',
width: (safeArea?.width || 0) + 'px',
height: (safeArea?.height || 0) + 'px',
} : {
width: 0 + 'rpx',
height: 0 + 'rpx',
}"></svga-player>
</template>
<script setup lang="ts">
import { ref, watch, computed, reactive } from 'vue';
import { downloadAndSaveToPath } from '@/uni_modules/tuikit-atomic-x/components/GiftPlayer/giftService';
interface SafeAreaLike {
top ?: number;
left ?: number;
width ?: number;
height ?: number;
}
interface GiftModel {
resourceURL : string;
name : string;
giftID : string | number;
isFromSelf : boolean;
timestamp : number;
cacheKey ?: string;
}
const props = defineProps<{
modelValue : boolean;
url ?: string;
safeArea ?: SafeAreaLike;
autoHideMs ?: number;
maxQueueSize ?: number;
}>();
const emit = defineEmits<{
(e : 'update:modelValue', value : boolean) : void
(e : 'finished') : void
}>();
const playerRef = ref<any>(null);
const internalUrl = ref<string>('');
const computedUrl = computed(() => internalUrl.value || props.url || '');
const MAX_CACHE_SIZE = props.maxQueueSize !== undefined ? props.maxQueueSize : 50;
const ABSOLUTE_MAX_SIZE = 1000;
const effectiveMaxSize = MAX_CACHE_SIZE === 0 ? ABSOLUTE_MAX_SIZE : Math.min(MAX_CACHE_SIZE, ABSOLUTE_MAX_SIZE);
const giftPrepareList = reactive<GiftModel[]>([]);
const isPlaying = ref<boolean>(false);
const currentPlayingGift = ref<GiftModel | null>(null);
async function resetPlayer() {
if (playerRef.value) {
try {
playerRef.value.stopPlay && playerRef.value.stopPlay();
} catch (e) {
// ignore
}
}
internalUrl.value = '';
await new Promise(resolve => setTimeout(resolve, 100));
if (playerRef.value) {
try {
playerRef.value.stopPlay && playerRef.value.stopPlay();
} catch (e) {
// ignore
}
}
}
async function resolveSvgaLocalPath(resourceURL : string, cacheKey : string) : Promise<string> {
let cachedPath = plus.storage.getItem(cacheKey);
if (!cachedPath) {
try {
const filePath = await downloadAndSaveToPath(resourceURL);
if (!filePath || filePath.startsWith('http://') || filePath.startsWith('https://')) {
throw new Error(`Invalid local path: ${filePath}`);
}
plus.storage.setItem(cacheKey, filePath as string);
return filePath as string;
} catch (_) {
return resourceURL;
}
} else {
if (cachedPath.startsWith('http://') || cachedPath.startsWith('https://')) {
plus.storage.removeItem(cacheKey);
try {
const filePath = await downloadAndSaveToPath(resourceURL);
if (filePath && !filePath.startsWith('http')) {
plus.storage.setItem(cacheKey, filePath as string);
return filePath as string;
} else {
throw new Error(`Invalid local path: ${filePath}`);
}
} catch (_) {
return resourceURL;
}
}
return cachedPath;
}
}
function addGiftToQueue(giftData : { resourceURL ?: string; name ?: string; giftID ?: string | number }, isFromSelf = false) {
if (!giftData || !giftData.resourceURL) return;
const giftDataCopy = {
resourceURL: String(giftData.resourceURL || ''),
name: String(giftData.name || ''),
giftID: giftData.giftID || 0,
};
const buildCacheKey = (resourceURL : string, name : string, giftID : string | number) => {
const giftIdStr = String(giftID || '');
let urlHash = '';
try {
const urlObj = new URL(resourceURL);
const fileName = (urlObj.pathname.split('/').pop() || '').replace(/\.(svga|SVGA)$/, '');
urlHash = fileName;
} catch (_) {
urlHash = resourceURL.substring(resourceURL.lastIndexOf('/') + 1).replace(/\.(svga|SVGA)$/i, '');
}
const fallbackName = (name || '').replace(/\s+/g, '').substring(0, 10);
return `${giftIdStr}-${(urlHash || fallbackName)}`;
};
const giftModel : GiftModel = {
resourceURL: giftDataCopy.resourceURL,
name: giftDataCopy.name,
giftID: giftDataCopy.giftID,
isFromSelf,
timestamp: Date.now(),
cacheKey: buildCacheKey(giftDataCopy.resourceURL, giftDataCopy.name, giftDataCopy.giftID),
};
if (isFromSelf) {
let hasNonSelfGift = false;
for (let i = 0; i < giftPrepareList.length; i++) {
if (!giftPrepareList[i].isFromSelf) {
hasNonSelfGift = true;
break;
}
}
if (hasNonSelfGift) {
let insertIndex = 0;
for (let i = giftPrepareList.length - 1; i >= 0; i--) {
if (giftPrepareList[i].isFromSelf) {
insertIndex = i + 1;
break;
}
}
giftPrepareList.splice(insertIndex, 0, giftModel);
} else {
giftPrepareList.push(giftModel);
}
} else {
giftPrepareList.push(giftModel);
}
while (giftPrepareList.length > effectiveMaxSize) {
let removed = false;
for (let i = 0; i < giftPrepareList.length; i++) {
if (!giftPrepareList[i].isFromSelf) {
giftPrepareList.splice(i, 1);
removed = true;
break;
}
}
if (!removed && giftPrepareList.length > effectiveMaxSize) {
giftPrepareList.shift();
}
}
if (giftPrepareList.length === 1 && !isPlaying.value) {
preparePlay();
}
}
async function preparePlay() {
if (giftPrepareList.length === 0) {
isPlaying.value = false;
return;
}
if (isPlaying.value) {
return;
}
await resetPlayer();
if (isPlaying.value) {
return;
}
isPlaying.value = true;
const queueGift = giftPrepareList[0];
const gift : GiftModel = {
resourceURL: String(queueGift.resourceURL || ''),
name: String(queueGift.name || ''),
giftID: queueGift.giftID || 0,
isFromSelf: queueGift.isFromSelf,
timestamp: queueGift.timestamp,
cacheKey: String(queueGift.cacheKey || ''),
};
currentPlayingGift.value = gift;
if (!gift || !gift.resourceURL) {
onPlayFinished();
return;
}
try {
const giftResourceURL = String(gift.resourceURL || '');
const giftCacheKey = String(gift.cacheKey || '');
const giftID = gift.giftID || 0;
let svgaGiftSourceUrl = await resolveSvgaLocalPath(giftResourceURL, giftCacheKey);
await new Promise(resolve => setTimeout(resolve, 50));
let newUrl = svgaGiftSourceUrl as string;
if (internalUrl.value === newUrl) {
internalUrl.value = '';
await new Promise(resolve => setTimeout(resolve, 50));
}
if (newUrl.startsWith('http://') || newUrl.startsWith('https://')) {
try {
const filePath = await downloadAndSaveToPath(giftResourceURL);
if (filePath && !filePath.startsWith('http')) {
plus.storage.setItem(giftCacheKey, filePath);
newUrl = filePath;
}
} catch (e) {
}
}
internalUrl.value = newUrl;
emit('update:modelValue', true);
setTimeout(() => {
const currentGift = currentPlayingGift.value;
if (props.modelValue && computedUrl.value && playerRef.value && isPlaying.value &&
currentGift && currentGift.giftID === giftID && currentGift.cacheKey === giftCacheKey) {
tryStart();
} else {
setTimeout(() => {
const retryGift = currentPlayingGift.value;
if (props.modelValue && computedUrl.value && playerRef.value && isPlaying.value &&
retryGift && retryGift.giftID === giftID && retryGift.cacheKey === giftCacheKey) {
tryStart();
}
}, 100);
}
}, 200);
} catch (e) {
onPlayFinished();
}
}
function onPlayFinished() {
isHandlingFinished = false;
if (playerRef.value) {
try {
playerRef.value.stopPlay && playerRef.value.stopPlay();
} catch (e) {
// ignore
}
}
isPlaying.value = false;
if (giftPrepareList.length > 0) {
giftPrepareList.shift();
}
currentPlayingGift.value = null;
internalUrl.value = '';
if (giftPrepareList.length > 0) {
setTimeout(() => {
if (playerRef.value) {
try {
playerRef.value.stopPlay && playerRef.value.stopPlay();
} catch (e) {
// ignore
}
}
if (!isPlaying.value) {
preparePlay();
}
}, 300);
} else {
emit('finished');
emit('update:modelValue', false);
}
}
const tryStart = () => {
if (props.modelValue && computedUrl.value && playerRef.value) {
const urlToPlay = computedUrl.value;
if (urlToPlay.startsWith('http://') || urlToPlay.startsWith('https://')) {
return;
}
try {
playerRef.value.startPlay(urlToPlay);
} catch (e) {
// ignore
}
}
}
watch(() => computedUrl.value, (newUrl) => {
if (!isPlaying.value && newUrl === computedUrl.value) {
tryStart();
}
});
let finishedTimer : NodeJS.Timer | null = null;
let isHandlingFinished = false;
const handleFinished = () => {
if (isHandlingFinished) {
return;
}
if (finishedTimer) {
clearTimeout(finishedTimer);
}
isHandlingFinished = true;
finishedTimer = setTimeout(() => {
onPlayFinished();
isHandlingFinished = false;
finishedTimer = null;
}, 50);
}
async function playGift(
giftData : { resourceURL ?: string; name ?: string; giftID ?: string | number },
isFromSelf = false
) {
addGiftToQueue(giftData, isFromSelf);
}
defineExpose({
playGift,
});
</script>
<style>
</style>

View File

@@ -0,0 +1,99 @@
import { ref } from 'vue'
import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState'
type GiftData = {
giftID?: string
name?: string
iconURL?: string
resourceURL?: string
coins?: number
[k: string]: any
}
export function giftService(params: {
roomId: string
giftPlayerRef: any
giftToastRef?: any
autoHideMs?: number
}) {
const { sendGift } = useGiftState(uni?.$liveID)
const isGiftPlaying = ref(false)
const showGift = async (giftData: GiftData, options?: { onlyDisplay?: boolean; isFromSelf?: boolean }) => {
if (!giftData) return
const onlyDisplay = !!options?.onlyDisplay
const isFromSelf = !!options?.isFromSelf // 是否为自送礼物(用户自己送的)
// SVGA 类型
if (giftData.resourceURL) {
isGiftPlaying.value = true
// 传递 isFromSelf 参数,用于队列优先级管理
params.giftPlayerRef?.value?.playGift(giftData, isFromSelf)
} else if (params.giftToastRef?.value) {
// 普通礼物提示
params.giftToastRef?.value?.showToast({
...giftData,
duration: 1500,
})
}
if (!onlyDisplay) {
sendGift({
liveID: params.roomId,
giftID: giftData.giftID,
count: 1,
success: () => { },
fail: () => { },
})
}
}
const onGiftFinished = () => {
isGiftPlaying.value = false
}
return {
showGift,
onGiftFinished,
isGiftPlaying,
}
}
/**
* 下载文件并保存到自定义路径
* @param {string} url - 文件网络地址
* @return {Promise<string>} 返回文件本地绝对路径
*/
export function downloadAndSaveToPath(url: string) {
return new Promise((resolve, reject) => {
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error('下载失败'))
return
}
let imageFilePath = ''
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (res) => {
imageFilePath = res.savedFilePath
// 转换为本地文件系统 URL
if (plus && plus.io && plus.io.convertLocalFileSystemURL) {
imageFilePath = plus.io.convertLocalFileSystemURL(imageFilePath)
}
resolve(imageFilePath)
},
fail: (err) => {
reject(new Error('保存文件失败'))
},
})
},
fail: (err) => {
reject(err)
},
})
})
}

View File

@@ -0,0 +1,671 @@
<template>
<view class="like-container">
<!-- 点赞按钮 -->
<view class="action-btn-wrapper" @click="handleLikeClick" @touchstart="handleTouchStart" v-if="role !== 'anchor' ">
<image class="action-btn" src="/static/images/live-like.png" />
</view>
<!-- 点赞动画容器 - 平台适配 -->
<view class="like-animations-container" :class="{ 'ios-container': isIOS, 'android-container': !isIOS }" :style="{
width: likeAnimations.length > 0 ? '400rpx' : '0',
height: likeAnimations.length > 0 ? '600rpx' : '0'
}">
<view v-for="(like, index) in likeAnimations" :key="like.id" class="like-animation"
:class="{ 'ios-animation': isIOS, 'android-animation': !isIOS }" :style="getAnimationStyle(like)">
<image class="heart-icon" :src="like.imageSrc" mode="aspectFit" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
import { useLikeState } from "@/uni_modules/tuikit-atomic-x/state/LikeState";
// 配置选项
const props = defineProps({
// 动画方案选择:'platform' - 平台适配方案, 'simple' - 统一兼容方案
animationMode: {
type: String,
default: 'platform',
validator: (value) => ['platform', 'simple'].includes(value)
},
// 角色anchor 主播 | audience 观众,用于设置默认行为或样式
role: {
type: String,
default: 'audience',
validator: (value) => ['anchor', 'audience'].includes(value)
},
// 容器位置
position: {
type: String,
default: 'bottom-right',
validator: (v) => ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(v)
},
// 同屏最大漂浮数量
maxConcurrent: {
type: Number,
default: 20,
},
// 是否启用触觉反馈
enableHaptics: {
type: Boolean,
default: true,
},
});
const { sendLike, addLikeListener, totalLikeCount, removeLikeListener } = useLikeState(uni.$liveID)
// 平台检测
const systemInfo = ref({});
const isIOS = computed(() => systemInfo.value.platform === 'ios');
// 初始化系统信息
uni.getSystemInfo({
success: (res) => {
systemInfo.value = res;
console.log('系统信息:', res);
}
});
// 点赞动画相关状态
const likeAnimations = ref([]);
let likeAnimationId = 0;
const currentLikeCount = ref(0);
let lastClickTime = 0; // 记录上次点击时间
const CLICK_INTERVAL = 100; // 点击间隔时间(毫秒)
// 批量点赞策略相关状态
const pendingLikeCount = ref(0); // 待发送的点赞数量
const batchTimer = ref(null); // 批量发送定时器
const BATCH_DELAY = 6000; // 6秒批量发送延迟
const lastSendTime = ref(0); // 记录上次发送时间
const isFirstClick = ref(true); // 是否第一次点击
// 存储每次接收到的总点赞数
const lastTotalLikesReceived = ref(0); // 上次接收到的总点赞数
// 心形图片数组
const heartImages = [
'/static/images/gift_heart0.png',
'/static/images/gift_heart1.png',
'/static/images/gift_heart2.png',
'/static/images/gift_heart3.png',
'/static/images/gift_heart4.png',
'/static/images/gift_heart5.png',
'/static/images/gift_heart6.png',
'/static/images/gift_heart7.png',
'/static/images/gift_heart8.png'
];
onMounted(() => {
addLikeListener(uni.$liveID, 'onReceiveLikesMessage', handleReceiveLikesMessage)
})
onUnmounted(() => {
removeLikeListener(uni.$liveID, 'onReceiveLikesMessage', handleReceiveLikesMessage)
// 清理定时器
if (batchTimer.value) {
clearTimeout(batchTimer.value);
batchTimer.value = null;
}
if (pendingLikeCount.value > 0) {
console.log('组件卸载,发送待发送的点赞(不显示动画)');
sendBatchLikes();
}
})
// 随机选择心形图片
const getRandomHeartImage = () => {
const randomIndex = Math.floor(Math.random() * heartImages.length);
return heartImages[randomIndex];
};
// 获取动画样式 - 平台适配
const getAnimationStyle = (like) => {
if (isIOS.value) {
// iOS端使用更简单的定位方式
return {
left: like.left + 'rpx',
top: like.top + 'rpx',
transform: like.transform,
opacity: like.opacity
};
} else {
// 安卓端使用原有方式
return {
left: like.left + 'rpx',
bottom: like.bottom + 'rpx',
transform: like.transform,
opacity: like.opacity
};
}
};
const createLikeAnimation = (count : number) => {
console.log('=== createLikeAnimation 开始 ===');
console.log('传入的count参数:', count);
console.log('当前动画数组长度:', likeAnimations.value.length);
console.log('当前动画数组:', JSON.stringify(likeAnimations.value));
// 固定创建3个动画忽略传入的count参数
const fixedCount = 3;
console.log('创建点赞动画,固定数量:', fixedCount, '平台:', isIOS.value ? 'iOS' : 'Android', '模式:', props.animationMode);
// 根据配置选择动画方案
if (props.animationMode === 'simple') {
console.log('使用简单动画方案');
createSimpleAnimation(fixedCount);
console.log('=== createLikeAnimation 结束(简单方案) ===');
return;
}
// 创建固定3个动画
const actualCount = Math.min(fixedCount, props.maxConcurrent); // 限制最大数量
console.log('实际创建动画数量:', actualCount);
for (let i = 0; i < actualCount; i++) {
const newLike = {
id: ++likeAnimationId,
show: true,
imageSrc: getRandomHeartImage(),
left: Math.random() * 120 + 40, // 随机水平位置
bottom: isIOS.value ? 0 : 60, // iOS端使用top定位Android端使用bottom定位
top: isIOS.value ? 400 : 0, // iOS端从顶部开始
transform: 'scale(0.8)',
opacity: 1
};
console.log('添加动画元素:', newLike);
// 控制最大并发,如果超过限制则移除最旧的
if (likeAnimations.value.length >= props.maxConcurrent) {
likeAnimations.value.shift();
}
likeAnimations.value.push(newLike);
// 平台适配的动画逻辑,添加延迟避免同时出现
const delay = i * 100; // 每个动画间隔100ms
setTimeout(() => {
if (isIOS.value) {
console.log('使用iOS动画方案');
createIOSAnimation(newLike);
} else {
console.log('使用Android动画方案');
createAndroidAnimation(newLike);
}
}, delay);
}
console.log('添加后动画数组长度:', likeAnimations.value.length);
console.log('=== createLikeAnimation 结束 ===');
};
// iOS端动画实现
const createIOSAnimation = (newLike) => {
// iOS端使用更简单的动画避免复杂的transition
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(1.2)';
like.top = like.top - 40;
console.log('iOS动画阶段1:', like.id);
}
}, 50);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(1)';
like.top = like.top - 40;
console.log('iOS动画阶段2:', like.id);
}
}, 150);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.9)';
like.top = like.top - 40;
like.opacity = 0.9;
console.log('iOS动画阶段3:', like.id);
}
}, 300);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.7)';
like.top = like.top - 40;
like.opacity = 0.7;
console.log('iOS动画阶段4:', like.id);
}
}, 450);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.5)';
like.top = like.top - 40;
like.opacity = 0.3;
console.log('iOS动画阶段5:', like.id);
}
}, 600);
// 动画结束后移除
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
likeAnimations.value.splice(index, 1);
console.log('移除iOS动画元素:', newLike.id);
console.log('移除后动画数组长度:', likeAnimations.value.length);
}
}, 1200);
};
// 安卓端动画实现
const createAndroidAnimation = (newLike) => {
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(1.2)';
like.bottom = like.bottom + 40;
console.log('Android动画阶段1:', like.id);
}
}, 100);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(1)';
like.bottom = like.bottom + 40;
console.log('Android动画阶段2:', like.id);
}
}, 200);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.9)';
like.bottom = like.bottom + 40;
like.opacity = 0.9;
console.log('Android动画阶段3:', like.id);
}
}, 400);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.7)';
like.bottom = like.bottom + 40;
like.opacity = 0.7;
console.log('Android动画阶段4:', like.id);
}
}, 600);
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.transform = 'scale(0.5)';
like.bottom = like.bottom + 40;
like.opacity = 0.3;
console.log('Android动画阶段5:', like.id);
}
}, 800);
// 动画结束后移除
setTimeout(() => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
likeAnimations.value.splice(index, 1);
console.log('移除Android动画元素:', newLike.id);
console.log('移除后动画数组长度:', likeAnimations.value.length);
}
}, 1500);
};
// 批量发送点赞
const sendBatchLikes = () => {
if (pendingLikeCount.value > 0) {
console.log('=== 批量发送点赞 ===');
console.log('发送数量:', pendingLikeCount.value);
sendLike({
liveID: uni.$liveID,
count: pendingLikeCount.value,
success: () => {
console.log('批量sendLike success, count:', pendingLikeCount.value);
// 更新发送时间
lastSendTime.value = Date.now();
isFirstClick.value = false;
},
fail: (code, msg) => {
console.error(`批量sendLike failed, code: ${code}, msg: ${msg}`);
},
});
// 清空待发送数量
pendingLikeCount.value = 0;
}
// 清除定时器
if (batchTimer.value) {
clearTimeout(batchTimer.value);
batchTimer.value = null;
}
};
// 处理点赞点击事件
const handleLikeClick = () => {
console.log('=== 点赞点击事件开始 ===');
console.log('当前时间:', new Date().toISOString());
console.log('当前动画数量:', likeAnimations.value.length);
console.log('待发送点赞数量:', pendingLikeCount.value);
// 添加点击间隔控制,确保每次点击只创建一个动画
const currentTime = Date.now();
if (currentTime - lastClickTime < CLICK_INTERVAL) {
console.log('点击间隔太短,跳过本次点击');
console.log('=== 点赞点击事件结束(跳过) ===');
return;
}
lastClickTime = currentTime;
// 添加平台检测和调试信息
const systemInfo = uni.getSystemInfoSync();
console.log('当前平台:', systemInfo.platform);
console.log('点击事件触发时间:', new Date().toISOString());
console.log('当前动画数量:', likeAnimations.value.length);
// 添加触觉反馈(仅安卓端)
if (props.enableHaptics && systemInfo.platform === 'android') {
uni.vibrateShort({
type: 'light'
});
}
// 智能发送策略:判断是否需要立即发送
const timeSinceLastSend = currentTime - lastSendTime.value;
const shouldSendImmediately = isFirstClick.value || timeSinceLastSend >= BATCH_DELAY;
console.log('智能发送判断:', {
isFirstClick: isFirstClick.value,
timeSinceLastSend: timeSinceLastSend,
shouldSendImmediately: shouldSendImmediately
});
// 累积点赞数量
pendingLikeCount.value += 1;
if (shouldSendImmediately) {
// 第一次点击或距离上次发送超过6秒立即发送
console.log('立即发送点赞');
sendBatchLikes();
// 立即发送时显示动画
console.log('立即发送,显示动画');
createLikeAnimation(3);
} else {
// 距离上次发送不足6秒累积并设置定时器
console.log('累积点赞,设置延迟发送');
// 6秒内点击不显示动画只累积数量
console.log('6秒内点击不显示动画只累积数量');
// 清除之前的定时器
if (batchTimer.value) {
clearTimeout(batchTimer.value);
}
// 计算剩余等待时间
const remainingTime = BATCH_DELAY - timeSinceLastSend;
console.log('剩余等待时间:', remainingTime, 'ms');
// 设置剩余时间的定时器
batchTimer.value = setTimeout(() => {
sendBatchLikes();
// 批量发送时显示动画
console.log('批量发送,显示动画');
createLikeAnimation(3);
}, remainingTime);
}
console.log('=== 点赞点击事件结束 ===');
};
// 对外暴露触发方法
function triggerLike(count : number = 1) {
createLikeAnimation(count);
}
defineExpose({ triggerLike });
// 处理触摸事件(安卓端备用方案)
const handleTouchStart = (event) => {
console.log('touch start event:', event);
// 防止重复触发
if (event && event.preventDefault) {
event.preventDefault();
}
// 触摸事件直接调用点赞逻辑间隔控制已在handleLikeClick中处理
if (event && event.touches && event.touches.length > 0) {
handleLikeClick();
}
};
// 方案二:统一兼容方案 - 使用更简单的动画实现
const createSimpleAnimation = (count : number) => {
console.log('=== createSimpleAnimation 开始 ===');
console.log('传入的count参数:', count);
console.log('当前动画数组长度:', likeAnimations.value.length);
// 固定创建3个动画忽略传入的count参数
const fixedCount = 3;
console.log('使用简单动画方案,固定数量:', fixedCount);
// 创建固定3个动画
const actualCount = Math.min(fixedCount, props.maxConcurrent);
console.log('实际创建简单动画数量:', actualCount);
for (let i = 0; i < actualCount; i++) {
const newLike = {
id: ++likeAnimationId,
show: true,
imageSrc: getRandomHeartImage(),
left: Math.random() * 120 + 40,
bottom: 60,
transform: 'scale(1)',
opacity: 1
};
console.log('添加简单动画元素:', newLike);
// 控制最大并发
if (likeAnimations.value.length >= props.maxConcurrent) {
likeAnimations.value.shift();
}
likeAnimations.value.push(newLike);
// 简化的动画逻辑,适用于两个平台,添加延迟避免同时出现
const delay = i * 100; // 每个动画间隔100ms
setTimeout(() => {
const animate = () => {
const index = likeAnimations.value.findIndex(item => item.id === newLike.id);
if (index > -1) {
const like = likeAnimations.value[index];
like.bottom = like.bottom + 20;
like.opacity = like.opacity - 0.1;
if (like.opacity > 0) {
setTimeout(animate, 100);
} else {
likeAnimations.value.splice(index, 1);
console.log('简单动画结束,移除元素:', newLike.id);
console.log('移除后动画数组长度:', likeAnimations.value.length);
}
}
};
animate();
}, delay);
}
console.log('添加后动画数组长度:', likeAnimations.value.length);
console.log('=== createSimpleAnimation 结束 ===');
};
const handleReceiveLikesMessage = {
callback: (event) => {
const res = JSON.parse(event)
if (res.sender.userID !== uni.$userID) {
createLikeAnimation(3);
}
}
}
watch(totalLikeCount, (newVal, oldVal) => {
if (oldVal) {
currentLikeCount.value = newVal - oldVal
}
}, {
deep: true,
immediate: true,
})
// 位置样式计算(用于简化模板内联 style 的拼接)
const containerInlineStyle = computed(() => {
const base = {
width: likeAnimations.value.length > 0 ? '400rpx' : '0',
height: likeAnimations.value.length > 0 ? '600rpx' : '0',
} as any;
switch (props.position) {
case 'bottom-left':
base.left = '40rpx';
base.bottom = '40rpx';
break;
case 'top-right':
base.right = '40rpx';
base.top = '40rpx';
break;
case 'top-left':
base.left = '40rpx';
base.top = '40rpx';
break;
default:
base.right = '40rpx';
base.bottom = '40rpx';
}
return base;
});
</script>
<style>
.like-container {
position: relative;
pointer-events: none;
}
.action-btn-wrapper {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 1000;
pointer-events: auto;
/* 增加点击区域 */
padding: 8rpx;
/* 确保在安卓端可以正常点击 */
min-height: 44px;
min-width: 44px;
}
.action-btn {
width: 64rpx;
height: 64rpx;
pointer-events: none;
/* 确保图片正确显示 */
}
.heart-icon {
width: 60rpx;
height: 60rpx;
}
.like-animations-container {
position: fixed;
bottom: 40rpx;
right: 40rpx;
width: 0;
height: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
transition: all 0.3s ease;
}
/* iOS端特殊样式 */
.ios-container {
position: absolute;
bottom: 40rpx;
right: 40rpx;
width: 0;
height: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
transition: all 0.3s ease;
}
/* Android端特殊样式 */
.android-container {
position: fixed;
bottom: 40rpx;
right: 40rpx;
width: 0;
height: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
transition: all 0.3s ease;
}
.like-animation {
position: absolute;
transition: all 0.1s ease-out;
pointer-events: none;
}
/* iOS端动画样式 */
.ios-animation {
position: absolute;
transition: none;
/* iOS端不使用transition避免兼容性问题 */
pointer-events: none;
}
/* Android端动画样式 */
.android-animation {
position: absolute;
transition: all 0.1s ease-out;
pointer-events: none;
}
.heart-icon {
width: 60rpx;
height: 60rpx;
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="drawer-header">
<view class="user-info">
<image class="user-avatar" :src="userInfo?.avatarURL || defaultAvatarURL" mode="aspectFill" />
<view class="user-details">
<view class="name-badge-row">
<text class="user-name">{{ userInfo?.userName || userInfo?.userID || '' }}</text>
<!-- <view class="badge">
<image class="badge-icon" src="/static/images/heart.png" mode="aspectFit" />
<text class="badge-text">65</text>
</view> -->
</view>
<text class="user-id">ID: {{ userInfo?.userID || '' }}</text>
</view>
</view>
<!-- <view class="follow-button" @tap="followUser">
<text class="follow-text">Follow</text>
</view> -->
</view>
<view class="drawer-content">
<view class="drawer-actions">
<!-- <view class="action-btn" @tap="muteSpeak">
<view class="action-btn-image-container">
<image class="action-btn-image" v-if="userInfo?.isMessageDisabled" src="/static/images/unmute-speak.png"
mode="aspectFit" />
<image class="action-btn-image" v-else src="/static/images/mute-speak.png" mode="aspectFit" />
</view>
<text class="action-btn-content" v-if="userInfo?.isMessageDisabled">解除禁言</text>
<text class="action-btn-content" v-else>禁言</text>
</view> -->
<view class="action-btn" @tap="kickOut">
<view class="action-btn-image-container">
<image class="action-btn-image" src="/static/images/kick-out-room.png" mode="aspectFit" />
</view>
<text class="action-btn-content">踢出房间</text>
</view>
</view>
<!-- <view class="divider-line-container">
<view class="divider-line"></view>
</view> -->
</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
import {
useLiveAudienceState
} from "@/uni_modules/tuikit-atomic-x/state/LiveAudienceState";
const {
currentLive
} = useLiveListState();
const {
setAdministrator,
revokeAdministrator,
kickUserOutOfRoom,
disableSendMessage
} = useLiveAudienceState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
userInfo: {
type: Object,
},
liveID: {
type: String,
}
});
const emit = defineEmits(['update:modelValue']);
const close = () => {
emit('update:modelValue', false);
};
const muteSpeak = () => {
console.log(
`mute or unMute speak, liveID: ${props.liveID}, isMessageDisabled: ${props?.userInfo?.isMessageDisabled}`);
const params = {
liveID: uni?.$liveID,
userID: props?.userInfo?.userID,
isDisable: !props?.userInfo?.isMessageDisabled,
};
if (props?.userInfo?.isMessageDisabled) {
disableSendMessage(params);
} else {
disableSendMessage(params);
}
close();
};
const kickOut = () => {
console.log('kick out from room', props?.userInfo?.userID);
uni.showModal({
content: `确认踢出${props?.userInfo?.userName || props?.userInfo?.userID}吗?`,
success: (res) => {
if (res.confirm) {
kickUserOutOfRoom({
liveID: uni?.$liveID,
userID: props?.userInfo?.userID,
success: () => {
close()
console.log(`kickUserOutOfRoom success`);
},
fail: (errCode, errMsg) => {
console.log(`kickUserOutOfRoom fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
}
}
});
};
const followUser = () => {
console.warn('== 关注用户 ', userInfo?.userName);
// 这里可以添加关注用户的逻辑
uni.showToast({
title: '关注成功',
icon: 'success'
});
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
height: 400rpx;
flex-direction: column;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
padding: 40rpx 48rpx;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
border-width: 2rpx;
border-color: #ffffff;
margin-right: 20rpx;
}
.user-details {
display: flex;
flex-direction: column;
}
.name-badge-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8rpx;
}
.user-name {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
margin-right: 16rpx;
}
.badge {
display: flex;
flex-direction: row;
align-items: center;
background-color: #8B5CF6;
padding: 4rpx 12rpx;
border-radius: 16rpx;
}
.badge-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.badge-text {
color: #ffffff;
font-size: 24rpx;
font-weight: 500;
}
.user-id {
color: rgba(255, 255, 255, 0.7);
font-size: 24rpx;
}
.follow-button {
background-color: #007AFF;
padding: 12rpx 32rpx;
border-radius: 32rpx;
/* height: 64rpx; */
}
.follow-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 500;
}
.drawer-content {
height: 400rpx;
justify-content: flex-start;
padding: 0 48rpx;
}
.drawer-actions {
display: flex;
flex-direction: row;
}
.action-btn {
flex-direction: column;
align-items: center;
margin-left: 10rpx;
height: 160rpx;
width: 120rpx
}
.action-btn-image-container {
width: 100rpx;
height: 100rpx;
background-color: rgba(43, 44, 48, 1);
margin-bottom: 16rpx;
border-radius: 20rpx;
justify-content: center;
align-items: center;
}
.action-btn-image {
width: 50rpx;
height: 50rpx;
}
.action-btn-content {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.divider-line-container {
height: 68rpx;
justify-content: center;
position: relative;
}
.divider-line {
width: 268rpx;
height: 10rpx;
border-radius: 200rpx;
background-color: #ffffff;
position: absolute;
bottom: 16rpx;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="live-audience-list">
<view class="audience-header">
<text class="audience-title">在线观众</text>
</view>
<scroll-view class="audience-content" scroll-y @scroll="handleScroll" :scroll-top="scrollTop">
<view v-if="audienceList.length > 0" class="audience-grid">
<view v-for="audience in audienceList" :key="audience.userID" class="audience-item">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" :src="audience.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ audience.userName || audience.userID }}</text>
<view v-if="audience.tag" class="audience-tag">
<text class="tag-text">{{ audience.tag }}</text>
</view>
</view>
<view class="audience-more" v-if="loginUserInfo?.userID === currentLive.liveOwner.userID"
@tap="audienceOperator(audience)">
<text class="more-text">···</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="audienceList.length === 0" class="empty-state">
<text class="empty-text">暂无观众</text>
<view></view>
</view>
<view v-if="isLoading" class="loading-state">
<image src="/static/images/loading.png" mode="aspectFit" class="loading-image" />
</view>
</scroll-view>
</view>
</view>
<AudienceActionPanel v-model="isShowAudienceActionPanel" :userInfo="selectedAudience" :liveID="liveID">
</AudienceActionPanel>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue';
import AudienceActionPanel from '@/uni_modules/tuikit-atomic-x/components/LiveAudienceList/AudienceActionPanel.nvue';
import {
useLiveAudienceState
} from "@/uni_modules/tuikit-atomic-x/state/LiveAudienceState";
import {
useLoginState
} from "@/uni_modules/tuikit-atomic-x/state/LoginState";
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
const {
currentLive
} = useLiveListState();
const {
loginUserInfo
} = useLoginState();
const {
audienceList,
audienceListCursor
} = useLiveAudienceState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const isLoading = ref(false);
const currentCursor = ref(0);
const scrollTop = ref(0);
const isShowAudienceActionPanel = ref(false);
const selectedAudience = ref(null);
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750,
});
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
currentViewerCount: {
type: Number,
default: 0,
},
liveID: {
type: String,
}
});
const emit = defineEmits(['update:modelValue']);
const close = () => {
emit('update:modelValue', false);
};
// 初始化加载
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
safeArea.value = res.safeArea;
}
});
});
const handleScroll = (e) => {
if (currentCursor.value === 0) return;
const {
scrollHeight,
scrollTop: currentScrollTop
} = e.detail;
scrollTop.value = currentScrollTop;
if (scrollHeight - currentScrollTop < 100) {
// loadAudiences(currentCursor.value);
}
};
const audienceOperator = (audience) => {
selectedAudience.value = audience;
isShowAudienceActionPanel.value = true;
};
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #1F2024;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
height: 1000rpx;
}
.drawer-open {
transform: translateY(0);
}
.live-audience-list {
flex: 1;
}
.audience-header {
padding: 32rpx;
flex-direction: row;
align-items: center;
display: flex;
height: 100rpx;
position: relative;
}
.empty-state {
padding: 64rpx;
justify-content: center;
align-items: center;
}
.empty-text {
color: #999999;
font-size: 28rpx;
}
.audience-title {
font-size: 32rpx;
color: #ffffff;
font-weight: 400;
flex: 1;
text-align: center;
line-height: 100rpx;
}
.audience-content {
flex: 1;
padding: 0 32rpx;
}
.audience-grid {
flex-direction: column;
}
.audience-item {
padding: 16rpx 0;
}
.audience-info {
flex-direction: row;
align-items: center;
}
.audience-avatar-container {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
}
.audience-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.audience-item-right {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.audience-detail {
flex-direction: row;
align-items: center;
}
.audience-name {
font-size: 28rpx;
color: #ffffff;
margin-right: 12rpx;
max-width: 300rpx;
lines: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.audience-tag {
background-color: #007AFF;
border-radius: 6rpx;
padding: 4rpx 12rpx;
}
.tag-text {
color: #ffffff;
font-size: 24rpx;
}
.audience-more {
padding: 0 20rpx;
}
.more-text {
font-size: 40rpx;
color: #ffffff;
font-weight: bold;
}
.empty-state,
.loading-state {
padding: 32rpx;
justify-content: center;
align-items: center;
}
.empty-text {
color: rgba(255, 255, 255, 0.6);
font-size: 28rpx;
}
.loading-image {
width: 48rpx;
height: 48rpx;
}
</style>

View File

@@ -0,0 +1,375 @@
<template>
<view class="live-list">
<view class="live-list-quick-join">
<input v-model="inputLiveId" class="quick-join-input" placeholder="输入房间名称、主播名称或房间ID" confirm-type="done"
@confirm="handleJoinLiveById" maxlength="64" />
</view>
<!-- 无匹配结果提示 -->
<view v-if="hasNoResults" class="empty-container">
<text class="empty-text">未找到匹配的直播</text>
</view>
<!-- 直播列表内容 -->
<list v-else class="live-list-content" @loadmore="loadMore" :show-scrollbar="false">
<cell v-for="(row, rowIndex) in groupedLiveList" :key="`row-${rowIndex}-${row[0]?.liveID || 'empty'}`">
<view class="live-row">
<view v-for="(live, index) in row" :key="live.liveID" class="live-card" @tap="handleJoinLive(live)">
<view class="live-cover">
<image :src="live.coverURL || defaultCoverURL" :mode="'aspectFill'" class="cover-image"
@error="handleCoverError(live)" />
<view class="live-status">
<view class="live-bar-container">
<view class="bar bar-1"></view>
<view class="bar bar-2"></view>
<view class="bar bar-3"></view>
</view>
<text class="viewer-count">{{ formatViewerCount(live.totalViewerCount) }}人看过</text>
</view>
<!-- 直播信息覆盖在封面图底部 -->
<view class="live-info-overlay">
<text class="live-title" :lines="1">{{ live.liveName }}</text>
<view class="live-owner">
<image :src="live?.liveOwner?.avatarURL || defaultAvatarURL" class="owner-avatar" mode="aspectFill" />
<text class="owner-name"
:numberOfLines="1">{{ live?.liveOwner?.userName || live?.liveOwner?.userID}}</text>
</view>
</view>
</view>
</view>
</view>
</cell>
</list>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
const {
liveList,
liveListCursor,
joinLive,
fetchLiveList
} = useLiveListState();
// 数据状态
const inputLiveId = ref(''); // 'live_'
const isLoadingJoin = ref(false);
// 默认图片
const defaultCoverURL =
'https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover1.png';
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
onMounted(() => {
const params = {
cursor: "", // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20, // 分页拉取的个数
};
fetchLiveList(params);
})
// 无匹配结果标记(用于 UI 展示)
const hasNoResults = computed(() => {
const k = (inputLiveId.value || '').trim();
return !!k && ((filteredLiveList.value || []).length === 0);
});
// 进入直播间
const handleJoinLive = async (live) => {
try {
uni.$liveID = live.liveID;
uni.redirectTo({
url: `/pages/audience/index?liveID=${live.liveID}`
});
} catch (error) {
console.error('进入直播间失败:', error);
uni.showToast({
title: '进入直播间失败',
icon: 'none'
});
}
};
const handleJoinLiveById = () => {
if (!inputLiveId.value.trim()) {
uni.showToast({
title: '请输入 liveID',
icon: 'none'
});
return;
}
uni.$liveID = inputLiveId.value.trim();
isLoadingJoin.value = true;
joinLive({
liveID: inputLiveId.value.trim(),
success: () => {
isLoadingJoin.value = false;
uni.redirectTo({
url: `/pages/audience/index?liveID=${inputLiveId.value.trim()}`,
});
},
fail: (error) => {
isLoadingJoin.value = false;
console.error('进房失败 = ', JSON.stringify(error))
uni.showToast({
title: "进入失败",
icon: "none",
content: error?.message || "房间不存在或已关闭",
});
},
});
};
const handleCoverError = (live) => {
live.coverURL = defaultCoverURL;
}
// 根据输入框的 liveID 关键字过滤直播列表(前端过滤,不请求后端)
const filteredLiveList = computed(() => {
const keyword = (inputLiveId.value || '').trim().toLowerCase();
if (!keyword) return liveList.value;
return (liveList.value || []).filter(item => {
const id = (item?.liveID || '').toLowerCase();
const name = (item?.liveName || '').toLowerCase();
const ownerName = (item?.liveOwner?.userName || item?.liveOwner?.userID || '').toLowerCase();
return id.includes(keyword) || name.includes(keyword) || ownerName.includes(keyword);
});
});
// 将直播列表分组,每行两个元素(对过滤后的结果进行分组)
const groupedLiveList = computed(() => {
const source = filteredLiveList.value || [];
const groups = [];
for (let i = 0; i < source.length; i += 2) {
const row = source.slice(i, i + 2);
groups.push(row);
}
return groups;
});
// 格式化观看人数
const formatViewerCount = (count) => {
if (count >= 10000) {
return `${(count / 10000).toFixed(1)}万`;
}
return count.toString();
};
// 加载更多
const loadMore = () => {
if (!liveListCursor.value) {
uni.showToast({
title: "没有更多了",
icon: "none"
});
return;
}
const params = {
cursor: liveListCursor.value,
count: 20,
};
fetchLiveList(params);
};
</script>
<style>
.live-list {
width: 750rpx;
background-color: #FFFFFF;
flex: 1;
}
.live-list-header {
padding: 32rpx;
}
.header-title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
}
.live-list-quick-join {
flex-direction: row;
align-items: center;
margin: 0 32rpx 24rpx 32rpx;
}
.quick-join-input {
flex: 1;
height: 64rpx;
border: 1rpx solid #e0e0e0;
border-radius: 999rpx;
padding: 10rpx 20rpx;
margin-top: 20rpx;
font-size: 28rpx;
background: #fff;
}
.live-list-content {
width: 750rpx;
}
.live-row {
flex-direction: row;
padding: 0 32rpx;
margin-bottom: 24rpx;
justify-content: space-between;
}
.live-card {
width: 334rpx;
height: 500rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
.live-cover {
position: relative;
width: 334rpx;
height: 500rpx;
}
.cover-image {
width: 334rpx;
height: 400rpx;
border-radius: 16rpx;
}
.live-status {
position: absolute;
top: 24rpx;
left: 20rpx;
flex-direction: row;
align-items: center;
}
.live-bar-container {
width: 16rpx;
height: 16rpx;
flex-direction: row;
justify-content: center;
align-items: flex-end;
margin-right: 8rpx;
}
.bar {
width: 4rpx;
background-color: #5AD69E;
}
.bar-1 {
height: 10rpx;
margin-right: 2rpx;
}
.bar-2 {
height: 16rpx;
margin-right: 2rpx;
}
.bar-3 {
height: 6rpx;
}
.viewer-count {
color: #ffffff;
font-size: 24rpx;
}
/* 新增:直播信息覆盖样式 */
.live-info-overlay {
position: absolute;
left: 0;
bottom: 0;
padding: 16rpx 20rpx;
width: 334rpx;
background-color: rgba(240, 242, 247, 1);
border-bottom-right-radius: 16rpx;
border-bottom-left-radius: 16rpx;
/* 半透明黑色背景 */
}
.live-info-overlay .live-title {
font-size: 32rpx;
font-weight: 600;
color: rgba(0, 0, 0, 0.9);
margin-bottom: 8rpx;
lines: 1;
max-width: 400rpx;
overflow: hidden;
text-overflow: ellipsis;
}
.live-info-overlay .live-owner {
flex-direction: row;
align-items: center;
}
.live-info-overlay .owner-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
margin-right: 8rpx;
}
.live-info-overlay .owner-name {
font-size: 24rpx;
color: rgba(0, 0, 0, 0.55);
font-weight: 500;
flex: 1;
lines: 1;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
}
.loading-container,
.empty-container,
.loading-more {
padding: 32rpx;
justify-content: center;
align-items: center;
}
.loading-icon {
width: 36rpx;
height: 36rpx;
color: #666666;
}
.loading-text,
.empty-text {
margin-top: 16rpx;
font-size: 28rpx;
color: #666666;
}
.load-more {
padding: 32rpx;
justify-content: center;
}
.load-more-text {
font-size: 28rpx;
color: #666666;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view v-if="modelValue" class="bottom-drawer-container">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<!-- 标题区域 -->
<view class="panel-header">
<text class="panel-title">网络信息</text>
</view>
<!-- 内容区域 -->
<view class="panel-content">
<!-- 视频状态 -->
<!-- <view class="row">
<image class="icon" src="/static/images/network-good.png" />
<view class="row-content">
<text class="row-title">视频状态正常</text>
<text class="row-desc">开播画面流畅 | 清晰度:{{ videoQuality }}</text>
</view>
</view>
<!-- 音频状态 -->
<!-- <view class="row">
<image class="icon" src="/static/images/beauty.png" />
<view class="row-content">
<text class="row-title">音频状态正常</text>
<text class="row-desc">适当音量保障观看体验 | 音质模式:{{ audioMode }}</text>
</view>
<slider class="audio-slider" :value="audioVolume" min="0" max="100" disabled />
<text class="audio-value">{{ audioVolume }}</text>
</view>
<!-- 设备温度 -->
<!-- <view class="row">
<image class="icon" src="/static/images/temperature.png" />
<view class="row-content">
<text class="row-title">设备温度正常</text>
<text class="row-desc">建议适时检查,保障观看体验</text>
</view>
</view> -->
<!-- 网络状态 -->
<!-- <view class="row">
<image class="icon" src="/static/images/network.png" />
<view class="row-content">
<text class="row-title">Wi-Fi/移动网络正常</text>
<text class="row-desc">建议不要频繁切换网络</text>
</view>
</view> -->
<!-- 网络指标区域 -->
<view class="metrics-section">
<view class="metrics-grid">
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.delay > 100, green: networkInfo.delay <= 100 }">{{ networkInfo.delay }}ms</text>
<text class="metric-label">往返延时</text>
</view>
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.downLoss > 0.1, green: networkInfo.downLoss <= 0.1 }">{{ networkInfo.downLoss }}%</text>
<text class="metric-label">下行丢包率</text>
</view>
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.upLoss > 0.1, green: networkInfo.upLoss <= 0.1 }">{{ networkInfo.upLoss }}%</text>
<text class="metric-label">上行丢包率</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { defineEmits } from 'vue';
import { useDeviceState } from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
const { networkInfo } = useDeviceState()
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false);
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
overflow: hidden;
/* 添加这行 */
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
}
.drawer-open {
transform: translateY(0);
}
/* 标题区域 */
.panel-header {
padding: 40rpx 0 20rpx;
background: rgba(34, 38, 46, 1);
align-items: center;
}
.panel-title {
font-size: 32rpx;
color: #ffffff;
font-weight: 500;
}
/* 内容区域 */
.panel-content {
flex: 1;
padding: 0 48rpx;
margin-bottom: 40rpx;
}
/* 网络指标区域 */
.metrics-section {
margin-bottom: 40rpx;
}
.metrics-grid {
flex-direction: row;
justify-content: space-between;
border-radius: 16rpx;
padding: 32rpx 24rpx;
background-color: rgba(43, 44, 48, 1);
}
.metric-item {
flex: 1;
align-items: center;
}
.metric-value {
font-size: 32rpx;
color: #4CAF50;
font-weight: 500;
margin-bottom: 12rpx;
}
.metric-value.red {
color: rgba(230, 89, 76, 1);
/* 红色 */
}
.metric-value.green {
color: rgba(56, 166, 115, 1);
/* 绿色 */
}
.metric-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
}
/* 保留原有样式,以备将来使用 */
.live-status-info-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.title {
color: #fff;
font-size: 32rpx;
font-weight: 500;
text-align: center;
padding-top: 20rpx
}
.row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 40rpx;
margin-right: 40rpx
}
.icon {
width: 40rpx;
height: 40rpx;
margin-right: 16rpx;
}
.row-content {
display: flex;
flex-direction: column;
flex: 1;
}
.row-title {
font-size: 28rpx;
font-weight: bold;
color: #fff;
}
.row-desc {
font-size: 24rpx;
color: #888;
margin-top: 4rpx;
}
.audio-slider {
flex: 1;
margin: 0 8rpx;
}
.audio-value {
font-size: 24rpx;
color: #222;
width: 40rpx;
text-align: right;
}
.network-info {
margin-bottom: 40rpx;
}
.network-grid {
flex-direction: row;
justify-content: space-between;
border-radius: 16rpx;
padding: 32rpx 24rpx;
background-color: rgba(43, 44, 48, 1);
}
.item {
flex: 1;
align-items: center;
}
.value {
font-size: 32rpx;
font-weight: 500;
color: white;
}
.value.red {
color: rgba(230, 89, 76, 1);
/* 红色 */
}
.value.green {
color: rgba(56, 166, 115, 1);
/* 绿色 */
}
.label {
font-size: 24rpx;
color: #888;
margin-top: 4rpx;
}
</style>

View File

@@ -0,0 +1,466 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }" @click.stop>
<view class="drawer-header">
<view class="user-info-section">
<view class="avatar-container">
<image class="user-avatar" :src="userInfo?.avatar || defaultAvatarURL" mode="aspectFill"></image>
</view>
<view class="user-details">
<text class="username">{{ userInfo?.userName || '' }}</text>
<text class="user-id">ID: {{ userInfo?.userID || '' }}</text>
</view>
</view>
</view>
<view class="drawer-content">
<view class="drawer-actions">
<view class="action-btn" @tap.stop="microphoneOperation">
<view class="action-btn-image-container">
<image class="action-btn-image" v-if="targetMicStatus !== 'OFF'" src="/static/images/mute-mic.png"
mode="aspectFit" />
<image class="action-btn-image" v-else src="/static/images/unmute-mic.png" mode="aspectFit" />
</view>
<text class="action-btn-content" v-if="targetMicStatus !== 'OFF'">{{ micOffText }}</text>
<text class="action-btn-content" v-else>{{ micOnText }}</text>
</view>
<view class="action-btn" @tap="cameraOperation">
<view class="action-btn-image-container">
<image class="action-btn-image" v-if="targetCameraStatus !== 'OFF'" src="/static/images/end-camera.png"
mode="aspectFit" />
<image class="action-btn-image" v-else src="/static/images/start-camera.png" mode="aspectFit" />
</view>
<text class="action-btn-content" v-if="targetCameraStatus !== 'OFF'">{{ cameraOffText }}</text>
<text class="action-btn-content" v-else>{{ cameraOnText }}</text>
</view>
<view class="action-btn" @tap="flipCamera" v-if="showFlip">
<view class="action-btn-image-container">
<image class="action-btn-image" src="/static/images/flip.png" mode="aspectFit" />
</view>
<text class="action-btn-content">翻转</text>
</view>
<view class="action-btn" @tap="handleHangUp" v-if="showHangup">
<view class="action-btn-image-container">
<image class="action-btn-image" src="/static/images/hangup.png" mode="aspectFit" />
</view>
<text class="action-btn-content">{{ hangupText }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch
} from 'vue';
import {
useDeviceState
} from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
import {
useCoGuestState
} from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
import {
useCoHostState
} from "@/uni_modules/tuikit-atomic-x/state/CoHostState";
import {
useLiveSeatState
} from "@/uni_modules/tuikit-atomic-x/state/LiveSeatState";
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 点击对象信息:{ userID, userName, seatIndex, avatar, ... }
userInfo: {
type: Object,
default: {}
},
liveID: {
type: String,
default: ''
},
// 展示模式:主播或观众
isAnchorMode: {
type: Boolean,
default: false
},
// 是否当前登录者本人(主播/观众自我操作用)
isSelf: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const close = () => emit('update:modelValue', false);
// 设备与连麦状态
const {
microphoneStatus,
cameraStatus,
isFrontCamera,
openLocalCamera,
closeLocalCamera,
openLocalMicrophone,
closeLocalMicrophone,
switchCamera,
} = useDeviceState(uni?.$liveID);
const {
exitHostConnection,
coHostStatus
} = useCoHostState(uni?.$liveID);
const {
disconnect
} = useCoGuestState(uni?.$liveID);
const {
seatList,
muteMicrophone,
unmuteMicrophone,
openRemoteCamera,
closeRemoteCamera,
openRemoteMicrophone,
closeRemoteMicrophone,
kickUserOutOfSeat
} = useLiveSeatState(uni?.$liveID);
// 控件显隐与文案
const showMic = computed(() => props.isSelf || (!props.isSelf && !props.isAnchorMode));
const showCamera = computed(() => props.isSelf || (!props.isSelf && !props.isAnchorMode));
const showFlip = computed(() => props.isSelf && targetCameraStatus.value === 'ON');
const showHangup = computed(() => !props.isSelf || (props.isSelf && !props.isAnchorMode));
const micOffText = computed(() => props.isAnchorMode && props.isSelf ? '静音' : '关闭音频');
const micOnText = computed(() => props.isAnchorMode && props.isSelf ? '解除静音' : '打开音频');
const cameraOffText = computed(() => props.isAnchorMode && props.isSelf ? '关闭视频' : '关闭视频');
const cameraOnText = computed(() => props.isAnchorMode && props.isSelf ? '打开视频' : '打开视频');
const hangupText = computed(() => props.isAnchorMode ? '移下麦' : '断开连麦');
const targetSeat = computed(() => {
const list = seatList.value || [];
const userID = props?.userInfo?.userID;
if (!userID || !Array.isArray(list)) return null;
return list.find(item => item?.userInfo?.userID === userID) || null;
});
const targetMicStatus = computed(() => {
return targetSeat.value?.userInfo?.microphoneStatus || 'OFF';
});
const targetCameraStatus = computed(() => {
return targetSeat.value?.userInfo?.cameraStatus || 'OFF';
});
// seatList 变化自动关闭(当目标用户离席)
watch(seatList, (newVal, oldVal) => {
if (!oldVal || !newVal || newVal.length === oldVal.length) return;
if (newVal.length < oldVal.length) {
const missing = oldVal.find(oldItem => !newVal.some(newItem => newItem.userInfo.userID === oldItem.userInfo
.userID));
if (missing && missing.userInfo?.userID === props?.userInfo?.userID) {
close();
}
}
}, {
deep: true,
immediate: true
});
// 操作区
const microphoneOperation = () => {
if (!props.isSelf) {
if (targetMicStatus.value === 'OFF') {
openRemoteMicrophone({
liveID: uni.$liveID,
userID: props.userInfo.userID,
policy: 'UNLOCK_ONLY'
});
uni.showToast({
title: `您已将${props.userInfo.userName || props.userInfo.userID} 解除静音`,
icon: 'none'
})
close();
return;
}
if (targetMicStatus.value === 'ON') {
closeRemoteMicrophone({
liveID: uni.$liveID,
userID: props.userInfo.userID,
});
uni.showToast({
title: `您已将${props.userInfo.userName || props.userInfo.userID} 静音`,
icon: 'none'
})
close();
return;
}
} else {
if (targetMicStatus.value === 'OFF') {
unmuteMicrophone({
liveID: uni.$liveID,
fail: (errorCode) => {
if (errorCode === -2360) {
uni.showToast({
title: '你已被静音',
icon: 'none'
})
}
}
});
close();
return;
}
if (targetMicStatus.value === 'ON') {
muteMicrophone({
liveID: uni.$liveID
});
close();
return;
}
}
};
const cameraOperation = () => {
if (!props.isSelf) {
if (targetCameraStatus.value === 'OFF') {
openRemoteCamera({
liveID: uni.$liveID,
userID: props.userInfo.userID,
policy: 'UNLOCK_ONLY'
});
uni.showToast({
title: `您已将${props.userInfo.userName || props.userInfo.userID} 解除禁画`,
icon: 'none'
})
close();
return;
}
if (targetCameraStatus.value === 'ON') {
closeRemoteCamera({
liveID: uni.$liveID,
userID: props.userInfo.userID,
});
uni.showToast({
title: `您已将${props.userInfo.userName || props.userInfo.userID} 禁画`,
icon: 'none'
})
close();
return;
}
} else {
if (targetCameraStatus.value === 'OFF') {
openLocalCamera({
isFront: isFrontCamera.value,
fail: (errorCode) => {
if (errorCode === -2370) {
uni.showToast({
title: '你已被禁画',
icon: 'none'
})
}
}
});
close();
return;
}
if (targetCameraStatus.value === 'ON') {
closeLocalCamera();
close();
return;
}
}
};
const flipCamera = () => {
if (cameraStatus.value !== 'ON' || !props.isSelf) return;
switchCamera({
isFront: !isFrontCamera.value
});
close();
};
const handleHangUp = () => {
if (props.isAnchorMode) {
// 主播模式:当自己处于主播连线时,提示断开与主播的连线;否则断开与观众的连麦
if (coHostStatus.value === 'CONNECTED') {
uni.showModal({
content: '确定要断开与其他主播的连线吗?',
confirmText: '断开',
success: (res) => {
if (res.confirm) {
exitHostConnection({
liveID: uni?.$liveID
});
}
close();
}
});
} else {
uni.showModal({
content: '确定要断开与其他观众的连麦吗?',
confirmText: '断开',
success: (res) => {
if (res.confirm) {
kickUserOutOfSeat({
liveID: uni?.$liveID,
userID: props.userInfo.userID,
success: () => {},
fail: () => {},
});
}
close();
}
});
}
} else {
// 观众模式:断开当前连麦
uni.showModal({
content: '确定要断开当前连麦吗?',
success: (res) => {
if (res.confirm) {
disconnect({
liveID: uni?.$liveID,
success: () => {},
fail: () => {},
});
close();
}
}
});
}
};
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(31, 32, 36, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
height: 400rpx;
flex-direction: column;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
padding: 40rpx 48rpx;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.user-info-section {
flex-direction: row;
align-items: center;
flex: 1;
}
.avatar-container {
width: 80rpx;
height: 80rpx;
margin-right: 24rpx;
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.user-details {
flex: 1;
flex-direction: column;
}
.username {
font-size: 32rpx;
color: #C5CCDB;
font-weight: 500;
margin-bottom: 8rpx;
}
.user-id {
font-size: 28rpx;
color: #7C85A6;
}
.drawer-content {
height: 400rpx;
justify-content: flex-start;
padding: 0 48rpx;
}
.drawer-actions {
display: flex;
flex-direction: row;
}
.action-btn {
flex-direction: column;
align-items: center;
height: 160rpx;
margin-left: 10rpx;
width: 120rpx
}
.action-btn-image-container {
width: 100rpx;
height: 100rpx;
background-color: rgba(43, 44, 48, 1);
margin-bottom: 16rpx;
border-radius: 20rpx;
justify-content: center;
align-items: center;
}
.action-btn-image {
width: 50rpx;
height: 50rpx;
}
.action-btn-content {
font-size: 26rpx;
color: #ffffff;
text-align: left;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,506 @@
<template>
<view class="live-stream-view-container">
<view class="live-stream-view-content" :class="{ 'audience-mode': !isAnchor }" @touchend.stop="" :style="{
top: isAnchor ? systemInfo.safeArea.top + 'px' : '0px',
left: isAnchor ? '0px' : systemInfo.safeArea.left + 'px',
width: isAnchor ? safeArea.width + 'px' : systemInfo.safeArea.width + 'px',
height: isAnchor ? systemInfo.safeArea?.height + 'px' : systemInfo.windowHeight + 'px',
}">
<!-- 主播模式:显示默认头像 -->
<view v-if="isAnchor && cameraStatus === 'OFF' && seatList.length === 1 && isLiving" class="default-avatar"
:style="{ top: safeArea.height * 1/2 + 'px' }">
<image style="width: 96rpx; height: 96rpx; border-radius: 99px;"
:src="loginUserInfo.avatarURL || defaultAvatarURL" />
</view>
<!-- 主视频流 -->
<live-core-view class="live-stream-view-background" :liveID="liveID"
:viewType=" isAnchor ? 'PUSH_VIEW' : 'PLAY_VIEW'" @tap="streamViewClick(loginUserInfo)" :style="{
top: '0px',
width: safeArea.width + 'px',
height: streamViewHeight + 'px',
}">
</live-core-view>
<!-- 单人模式(观众视角特有) -->
<view v-if="!isAnchor && seatList?.length === 1" @touchend.stop="" class="grid-content-cell"
v-for="(participant, colIndex) in seatList" :key="`${colIndex}`" @tap="streamViewClick(participant)" :style="{
left: participant?.region?.x * scale.scaleX + 'px',
top: (calculateTopValue(participant) - 2) + 'px',
'background-color': participant?.userInfo?.cameraStatus === 'OFF' ? '#000' : 'transparent',
}">
<view style="display: flex; flex-direction: row; width: 120rpx; justify-content: center; align-items: center;"
:style="{
width: participant?.region?.w * scale.scaleX + 'px',
height: (streamViewHeight + 10) + 'px',
}">
<view v-if="participant?.userInfo?.cameraStatus === 'OFF'" class="video-container">
<image class="participant-video" :src="participant?.userInfo?.avatarURL || defaultAvatarURL"
mode="aspectFill" />
</view>
</view>
</view>
<!-- 多人模式:非网格布局 -->
<view v-if="seatList?.length > 1 && (templateLayout !== 801 && templateLayout !== 800)" @touchend.stop=""
class="grid-content-cell" v-for="(participant, colIndex) in seatList" :key="`${colIndex}`"
@tap="streamViewClick(participant)" style="border-radius: 0rpx;" :style="{
left: participant?.region?.x * scale.scaleX + 'px',
top: calculateTopValue(participant) + (isAnchor ? 0 : -2) + 'px',
'background-color': !isAnchor && participant?.userInfo?.cameraStatus === 'OFF' ? '#000' : 'transparent',
}">
<view style="display: flex; flex-direction: row; width: 120rpx; justify-content: center; align-items: center;"
:style="{
width: participant?.region?.w * scale.scaleX + 'px',
height: isAnchor ? (participant?.region?.h * scale.scaleY + 'px') : (participant?.region?.h * scale.scaleY + 2) + 'px',
}">
<view v-if="participant?.userInfo?.cameraStatus === 'OFF'" class="video-container">
<image class="participant-video" :src="participant?.userInfo?.avatarURL || defaultAvatarURL"
mode="aspectFill" />
</view>
<view class="participant-info-container">
<image v-if="participant?.userInfo?.microphoneStatus === 'OFF'" class="mic-icon"
src="/static/images/unmute-mic.png" mode="aspectFit" />
<text class="participant-name" :style="{ 'max-width': participant?.region.w * scale.scaleX * 0.85 + 'px' }">
{{ participant?.userInfo?.userName || participant?.userInfo?.userID }}
</text>
</view>
</view>
</view>
<!-- 多人模式:网格布局 -->
<view v-if="seatList?.length > 1 && (templateLayout === 801 || templateLayout === 800)" @touchend.stop=""
class="grid-content-cell" v-for="(participant, colIndex) in seatList"
:key="`${isAnchor ? colIndex : participant.index}`" @tap="streamViewClick(participant)"
style="border-radius: 0rpx;" :style="{
left: participant?.region?.x * scale.scaleX + 'px',
top: calculateTopValue(participant) + (isAnchor ? 0 : -2) + 'px',
'background-color': !isAnchor && participant?.userInfo?.cameraStatus === 'OFF' ? '#000' : 'transparent',
}">
<view v-if="participant?.userInfo.userID"
style="display: flex; flex-direction: row; width: 120rpx; justify-content: center; align-items: center;"
:style="{
width: participant?.region?.w * scale.scaleX + 'px',
height: isAnchor ? (participant?.region?.h * scale.scaleY + 'px') : (participant?.region?.h * scale.scaleY + 2) + 'px',
}">
<view v-if="participant?.userInfo?.cameraStatus === 'OFF'" class="video-container">
<image class="participant-video" :src="participant?.userInfo?.avatarURL || defaultAvatarURL"
mode="aspectFill" />
</view>
<view class="participant-info-container" :class="{ 'audience-mode': !isAnchor }">
<image v-if="participant?.userInfo?.microphoneStatus === 'OFF'" class="mic-icon"
src="/static/images/unmute-mic.png" mode="aspectFit" />
<text class="participant-name" :style="{ 'max-width': participant?.region.w * scale.scaleX * 0.85 + 'px' }">
{{ participant?.userInfo?.userName || participant?.userInfo?.userID }}
</text>
</view>
</view>
<view class="participant-wait-container" v-else :style="{
width: participant?.region?.w * scale.scaleX + 'px',
height: participant?.region?.h * scale.scaleY + 'px',
'border-bottom': '1rpx solid #000',
'background-color': '#1f2024',
'border-left': isAnchor && templateLayout === 800 ? '1rpx solid #000' : '',
'border-right': !isAnchor && templateLayout === 800 ? '1rpx solid #000' : '',
}">
<text class="participant-wait-content">{{ isAnchor ? colIndex : participant?.index }}</text>
<text class="participant-wait-content">等待连麦</text>
</view>
</view>
<LiveStreamActionPanel v-if="enableClickPanel" v-model="isShowClickPanel" :liveID="liveID || ''"
:userInfo="clickUserInfo" :isAnchorMode="isAnchor"
:isSelf="(isAnchor ? (clickUserInfo?.userID === loginUserInfo?.userID) : (clickUserInfo?.userID === currentLoginUserId))" />
</view>
</view>
</template>
<script setup lang="ts">
import {
ref,
onMounted,
computed,
watch
} from 'vue';
import {
useLiveSeatState
} from "@/uni_modules/tuikit-atomic-x/state/LiveSeatState";
import {
useLoginState
} from "@/uni_modules/tuikit-atomic-x/state/LoginState";
import {
useCoHostState
} from "@/uni_modules/tuikit-atomic-x/state/CoHostState";
import {
useDeviceState
} from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
import {
useCoGuestState
} from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
import LiveStreamActionPanel from '@/uni_modules/tuikit-atomic-x/components/LiveStreamView/LiveStreamActionPanel.nvue';
const props = defineProps({
liveID: {
type: String,
default: '',
},
onStreamViewClick: {
type: Function,
default: null
},
isAnchor: {
type: Boolean,
default: true,
},
templateLayout: {
type: Number,
},
isLiving: {
type: Boolean,
default: false,
},
currentLoginUserId: {
type: String,
}
,
enableClickPanel: {
type: Boolean,
default: false,
}
});
const emit = defineEmits(['streamViewClick']);
// 状态管理
const {
loginUserInfo
} = useLoginState();
const {
connected,
coHostStatus
} = useCoHostState(uni?.$liveID);
const {
cameraStatus
} = useDeviceState();
const {
connected: audienceConnected
} = useCoGuestState(uni?.$liveID)
// 使用 rpx
const standardWidth = 750;
let standardHeight = standardWidth * 16 / 9;
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: standardWidth,
height: standardHeight,
});
const systemInfo = ref({});
const deviceWidthRatio = ref(1);
const deviceHeightRatio = ref(1);
const bottomPanelHeight = ref(80);
const pixelWidthRatio = ref(1);
const {
seatList,
canvas,
lockSeat,
} = props?.liveID && useLiveSeatState(props?.liveID);
const platform = ref('android');
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const scale = ref({
scaleX: 1,
scaleY: 1
});
const streamViewHeight = ref(1);
const isShowClickPanel = ref(false);
const clickUserInfo = ref<any>({});
function calculateScale(originalWidth : number, originalHeight : number, displayWidth : number, displayHeight : number) {
// 等比缩放(以宽度或高度中较小的比例为准)
const scaleX = displayWidth / originalWidth;
const scaleY = displayHeight / originalHeight;
return { scaleX, scaleY }; // 确保不变形
}
// 初始化加载
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
systemInfo.value = res;
safeArea.value = res.safeArea;
deviceWidthRatio.value = standardWidth / res.windowWidth;
platform.value = res.platform;
if (platform.value === 'ios') {
bottomPanelHeight.value = 60;
}
if (props.isAnchor) {
streamViewHeight.value = systemInfo.value.safeArea.height - bottomPanelHeight.value;
} else {
streamViewHeight.value = systemInfo.value.windowHeight;
}
console.warn(`LiveStreamView: isAnchor=${props.isAnchor}, safeArea: ${JSON.stringify(res.safeArea)},
deviceWidthRatio: ${deviceWidthRatio.value}, deviceHeightRatio: ${deviceHeightRatio.value},
windowWidth: ${res.windowWidth}, windowHeight: ${res.windowHeight},
streamViewHeight: ${streamViewHeight.value}`);
}
});
});
// 下发的视频像素值
watch(canvas, (newValue) => {
if (newValue?.w) {
pixelWidthRatio.value = (safeArea.value.width / newValue?.w) * deviceWidthRatio.value;
console.warn(`LiveStreamView: canvas 变动: ${pixelWidthRatio.value}`)
}
if (newValue?.w && newValue?.h) {
scale.value = calculateScale(
newValue.w,
newValue.h,
systemInfo.value.safeArea.width,
systemInfo.value.safeArea.width * newValue.h / newValue.w,
);
console.warn(
`LiveStreamView: scale value: ${JSON.stringify(scale.value)}, streamViewHeight: ${streamViewHeight.value}`
);
}
});
const streamViewClick = (options) => {
console.log(`LiveStreamView: click options: ${JSON.stringify(options)}, isAnchor: ${props.isAnchor}`);
if (!props.isLiving) return
// 非主播模式且没有参与者信息(背景点击),直接忽略
if (!props.isAnchor && !options?.userInfo) {
return;
}
if (props.isAnchor) {
// 主播模式区分点击背景loginUserInfo和点击参与者带 userInfo
if (options?.userInfo) {
if (audienceConnected.value.length > 1 && connected?.value.length === 0) {
const userInfo = { ...options.userInfo, seatIndex: options.index };
if (!userInfo?.userID) return;
if (props.enableClickPanel) {
clickUserInfo.value = userInfo;
isShowClickPanel.value = true;
}
}
if (connected?.value.length > 0) {
if (options?.userInfo.userID === uni.$userID) {
const userInfo = { ...options.userInfo, seatIndex: options.index };
if (!userInfo?.userID) return;
if (props.enableClickPanel) {
clickUserInfo.value = userInfo;
isShowClickPanel.value = true;
}
}
}
} else {
// 背景点击:直接把当前登录用户信息抛给上层,保持原有行为
// props.onStreamViewClick?.(options);
// emit('streamViewClick', options);
if (props.enableClickPanel) {
clickUserInfo.value = { ...(loginUserInfo.value || {}), userID: loginUserInfo.value?.userID } as any;
isShowClickPanel.value = true;
}
}
} else {
// 面板限制规则仅影响面板,不影响事件下发给页面
const isClickSelf = options?.userInfo?.userID === props?.currentLoginUserId;
const isTemplateGrid = props.templateLayout === 800 || props.templateLayout === 801;
const isCoGuesting = uni.$localGuestStatus !== 'DISCONNECTED';
const payload = { ...options.userInfo, seatIndex: options.index };
if (props.enableClickPanel && isClickSelf) {
clickUserInfo.value = payload as any;
isShowClickPanel.value = true;
return
}
props.onStreamViewClick?.(payload);
emit('streamViewClick', payload);
}
};
// 返回 px 对应的计算值
const calculateTopValue = (participant) => {
let topValue = 1;
console.log(
`LiveStreamView: templateLayout: ${props.templateLayout}, platform: ${platform.value}, isAnchor: ${props.isAnchor}`
);
if (!participant) return topValue;
if (props.templateLayout !== 800 && props.templateLayout !== 801) {
topValue = participant.region.y * scale.value.scaleY;
}
if (props.templateLayout === 800) {
topValue = participant.region.y * scale.value.scaleY;
}
if (props.templateLayout === 801) {
topValue = participant.region.y * scale.value.scaleY;
}
console.log(`LiveStreamView: topValue: ${topValue}`);
return topValue;
};
watch(seatList, (newValue) => {
if ((newValue || []).length > 1 && canvas.value?.w && canvas.value?.h) {
if (props.isAnchor) {
streamViewHeight.value = systemInfo.value.safeArea.height - bottomPanelHeight.value;
} else {
streamViewHeight.value = systemInfo.value.windowHeight;
}
}
if (!props.isAnchor && (newValue || []).length === 1 && canvas.value?.w && canvas.value?.h &&
audienceConnected.value.length === 1) {
streamViewHeight.value = systemInfo.value.windowHeight;
}
console.log(`LiveStreamView: seatList change streamViewHeight: ${streamViewHeight.value}`);
});
// 观众模式特有的状态监听
if (!props.isAnchor) {
watch(audienceConnected.value, (newVal, oldVal) => {
console.log(
`LiveStreamView: audience localStatus change, newVal: ${newVal}, oldVal: ${oldVal}, templateLayout: ${props.templateLayout}`
);
if (newVal.length === 1) {
streamViewHeight.value = systemInfo.value.windowHeight;
console.log(`LiveStreamView: audience localStatus change streamViewHeight: ${streamViewHeight.value}`);
}
});
}
defineExpose({
streamViewClick,
calculateTopValue,
scale,
streamViewHeight
});
</script>
<style>
.live-stream-view-container {
flex: 1;
position: relative;
background: rgba(15, 16, 20, 0.5);
z-index: -1;
overflow: hidden;
}
.live-stream-view-content {
flex: 1;
position: relative;
}
.live-stream-view-content.audience-mode {
background-color: #000;
}
.live-stream-view-background {
position: relative;
right: 0;
bottom: 0;
z-index: -1;
}
.grid-image-cell {
position: absolute;
border-radius: 24rpx;
overflow: hidden;
z-index: 1000;
}
.participant-video {
width: 80rpx;
height: 80rpx;
border-radius: 80rpx;
}
.grid-content-cell {
flex: 1;
position: absolute;
border-radius: 24rpx;
overflow: hidden;
}
.participant-info-container {
position: absolute;
display: flex;
flex-direction: row;
align-items: center;
background: rgba(34, 38, 46, 0.4);
border-radius: 38rpx;
height: 36rpx;
padding-left: 6rpx;
padding-right: 12rpx;
left: 6rpx;
bottom: 6rpx;
flex: 1;
}
.participant-info-container.audience-mode {
/* 观众模式下的特殊样式 */
}
.mic-icon {
width: 24rpx;
height: 24rpx;
margin-left: 4rpx;
background-color: rgba(34, 38, 46, 0);
}
.participant-name {
font-size: 24rpx;
font-weight: 500;
color: #ffffff;
margin-left: 2rpx;
text-align: center;
min-width: 80rpx;
lines: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.participant-wait-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.participant-wait-content {
font-size: 28rpx;
font-weight: 500;
text-align: center;
color: #ffffff;
margin-top: 12rpx;
}
.default-avatar {
position: sticky;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="avatar-container">
<image class="avatar" :src="userInfo?.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="user-info-container" v-if="isShowAnchor">
<text class="user-name">{{ userInfo?.userName || userInfo?.userID || '' }}</text>
<text class="user-roomid">直播房间ID{{ userInfo?.liveID || userInfo?.roomId }}</text>
</view>
<view class="user-info-container" v-if="!isShowAnchor">
<text class="user-name">{{ userInfo?.userName || userInfo?.userID || '' }}</text>
<text class="user-roomid">UserId{{ userInfo?.userID }}</text>
</view>
</view>
</view>
</template>
<script setup>
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: { type: Boolean, default: false },
userInfo: { type: Object, default: () => ({}) },
isShowAnchor: { type: Boolean, default: true }
});
const emit = defineEmits(['update:modelValue']);
const close = () => { emit('update:modelValue', false); };
</script>
<style>
.bottom-drawer-container { position: fixed; bottom: 0; left: 0; right: 0; top: 0; z-index: 1000; }
.drawer-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(15, 16, 20, 0.8); }
.bottom-drawer { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(34, 38, 46, 1); transition-property: transform; transition-duration: 0.3s; transition-timing-function: ease; flex-direction: column; align-items: center; height: 400rpx; padding: 20rpx 0; border-top-left-radius: 32rpx; border-top-right-radius: 32rpx; overflow: hidden; }
.drawer-open { transform: translateY(0); }
.avatar-container { width: 200rpx; height: 120rpx; justify-content: center; align-items: center; position: absolute; }
.avatar { width: 112rpx; height: 112rpx; border-radius: 56rpx; }
.user-info-container { flex: 1; padding-top: 120rpx; align-items: center; }
.user-name { font-size: 32rpx; color: rgba(255, 255, 255, 0.9); }
.user-roomid { font-size: 24rpx; color: rgba(255, 255, 255, 0.55); margin: 20rpx 0; }
.divider-line-container { height: 68rpx; justify-content: center; position: relative; align-items: center; }
.divider-line { width: 268rpx; height: 10rpx; border-radius: 200rpx; background-color: rgba(255, 255, 255, 1); position: absolute; bottom: 16rpx; }
</style>

View File

@@ -0,0 +1,282 @@
<template>
<!-- 仪表盘面板组件 -->
<!-- 使用方式: <NetworkQualityPanel v-model="showDashboardPanel" /> -->
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<!-- 标题区域 -->
<view class="panel-header">
<text class="panel-title">仪表盘</text>
</view>
<!-- 内容区域 -->
<view class="panel-content">
<!-- 网络指标区域 -->
<view class="metrics-section">
<view class="metrics-grid">
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.delay > 100, green: networkInfo.delay <= 100 }">{{ networkInfo.delay }}ms</text>
<text class="metric-label">往返延时</text>
</view>
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.downLoss > 0.1, green: networkInfo.downLoss <= 0.1 }">{{ networkInfo.downLoss }}%</text>
<text class="metric-label">下行丢包率</text>
</view>
<view class="metric-item">
<text class="metric-value"
:class="{ red: networkInfo.upLoss > 0.1, green: networkInfo.upLoss <= 0.1 }">{{ networkInfo.upLoss }}%</text>
<text class="metric-label">上行丢包率</text>
</view>
</view>
</view>
<!-- 视频和音频信息区域 -->
<!-- <view class="info-section"> -->
<!-- 视频信息 -->
<!-- <view class="info-card">
<text class="info-title">视频信息</text>
<view class="info-list">
<view class="info-item">
<text class="info-label">分辨率</text>
<text class="info-value">{{ videoInfo.resolution }}</text>
</view>
<view class="info-item">
<text class="info-label">码率</text>
<text class="info-value">{{ videoInfo.bitrate }}</text>
</view>
<view class="info-item">
<text class="info-label">帧率</text>
<text class="info-value">{{ videoInfo.frameRate }}</text>
</view>
</view>
</view> -->
<!-- 音频信息 -->
<!-- <view class="info-card">
<text class="info-title">音频信息</text>
<view class="info-list">
<view class="info-item">
<text class="info-label">采样率</text>
<text class="info-value">{{ audioInfo.sampleRate }}</text>
</view>
<view class="info-item">
<text class="info-label">码率</text>
<text class="info-value">{{ audioInfo.bitrate }}</text>
</view>
</view>
</view>
</view> -->
</view>
</view>
</view>
</template>
<script setup>
import {
reactive
} from 'vue'
import {
useDeviceState
} from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
const {
networkInfo
} = useDeviceState()
/**
* 仪表盘面板组件
*
* Props:
* - modelValue: Boolean - 控制面板显示/隐藏
*
* Events:
* - update:modelValue - 更新面板显示状态
*/
// 定义 props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 响应式数据
const networkMetrics = reactive({
roundTripDelay: 45,
downlinkPacketLoss: 0,
uplinkPacketLoss: 2
})
const videoInfo = reactive({
resolution: '1080*1920',
bitrate: '6015 kbps',
frameRate: '31 FPS'
})
const audioInfo = reactive({
sampleRate: '48000 HZ',
bitrate: '6015 kbps'
})
// 方法
/**
* 关闭面板
*/
const close = () => {
emit('update:modelValue', false)
}
</script>
<style>
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
}
.drawer-open {
transform: translateY(0);
}
/* 标题区域 */
.panel-header {
padding: 40rpx 0 20rpx;
background: rgba(34, 38, 46, 1);
align-items: center;
}
.panel-title {
font-size: 32rpx;
color: #ffffff;
font-weight: 500;
}
/* 内容区域 */
.panel-content {
flex: 1;
padding: 0 48rpx;
margin-bottom: 40rpx;
}
/* 网络指标区域 */
.metrics-section {
margin-bottom: 40rpx;
}
.metrics-grid {
flex-direction: row;
justify-content: space-between;
border-radius: 16rpx;
padding: 32rpx 24rpx;
background-color: rgba(43, 44, 48, 1);
}
.metric-item {
flex: 1;
align-items: center;
}
.metric-value {
font-size: 32rpx;
color: #4CAF50;
font-weight: 500;
margin-bottom: 12rpx;
}
.metric-value.red {
color: rgba(230, 89, 76, 1);
/* 红色 */
}
.metric-value.green {
color: rgba(56, 166, 115, 1);
/* 绿色 */
}
.metric-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
}
/* 信息区域 */
.info-section {
flex-direction: row;
justify-content: space-between;
}
.info-card {
flex: 1;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
padding: 24rpx;
margin: 0 8rpx;
}
.info-title {
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
margin-bottom: 20rpx;
text-align: center;
}
.info-list {
flex-direction: column;
}
.info-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: 26rpx;
color: #ffffff;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,101 @@
{
"id": "tuikit-atomic-x",
"displayName": "【官方】腾讯云实时音视频响应式 SDK",
"version": "1.1.3",
"description": "tuikit-atomic-x sdk 是腾讯云推出的面向即时通信、音视频通话、视频直播、语聊房等场景的响应式 API sdk支持房间管理、主播开播、观众观看、成员管理、麦位控制、基础美颜等功能。",
"keywords": [
"腾讯云",
"直播",
"语聊房",
"Android/iOS",
"即时通信"
],
"repository": "https://github.com/Tencent-RTC/TUIKit_uni-app",
"engines": {
"HBuilderX": "^3.7.0",
"uni-app": "^4.45",
"uni-app-x": "^4.45"
},
"dcloudext": {
"type": "component-uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "麦克风、摄像头"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": {},
"vue3": {}
},
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"vue": {},
"nvue": {},
"android": "x",
"ios": "x",
"harmony": "x"
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
# 腾讯云 · 在线直播解决方案
<img src="https://qcloudimg.tencent-cloud.cn/raw/ec034fc6e4cf42cae579d32f5ab434a1.png" align="left" width=65 height=65>TUILiveKit是一款适用于社交娱乐、购物、健身等互动直播场景的产品通过集成该产品仅需三步30分钟内就可以为您的App添加互动连麦、送礼、房间管理等功能。
## 产品特性
<p align="center">
<img src="https://web.sdk.qcloud.com/component/uni-app/assets/clipboard_20250903_054158.png"/>
</p>
- **完善的 UI 交互**:我们提供含 UI 的开源组件 TUILiveKit可以节省您 90% 开发时间您只需要花费30分钟就可以拥有一款类似抖音的在线直播应用。
- **多平台互联互通**我们支持Android、iOS、uni-app 平台,您可以使用不同平台的 TUILiveKit 组件支持在线直播业务,未来我们还计划支持 Flutter、MacOS、Windows等平台。
- **互动弹幕**:我们支持自定义文本,自定义表情,房间信息提醒等多种互动信息展示。
- **互动礼物**:我们支持自定义礼物动画展示,费用自定义结算等功能。
## 开始使用
这里以 含 UI 的集成即TUILiveKit为例这也是我们推荐的集成方式关键步骤如下
- **Step1**:接入 TUILiveKit 到您的项目中uni-app livekit 的[接入流程](https://cloud.tencent.com/document/product/647/105442)
- **Step2**:开启您的第一个在线直播体验
## 交流&反馈
如果您在使用过程中有遇到什么问题,欢迎提交 [**issue**](https://github.com/tencentyun/TUILiveRoom/issues)欢迎您加入我们的开发者QQ群进行技术交流和反馈问题QQ群 ID605115878.

View File

@@ -0,0 +1,253 @@
/**
* @module AudioEffectState
* @module_description
* 音效设置管理模块
* 核心功能:提供变声、混响、耳返等高级音效功能,支持多种音效效果和实时音效调节。
* 技术特点:基于音频处理算法,支持实时音效处理、低延迟音频传输、音质优化等高级技术。
* 业务价值:为直播平台提供差异化的音效体验,增强用户参与度和直播趣味性。
* 应用场景变声直播、K歌直播、音效娱乐、专业音效等需要音频处理的场景。
*/
import { ref } from "vue";
import {
SetAudioChangerTypeOptions, SetAudioReverbTypeOptions, SetVoiceEarMonitorEnableOptions,
VolumeOptions, AudioChangerTypeParam, AudioReverbTypeParam
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 变声器类型映射表
* @internal
*/
const CHANGER_TYPE_MAP: Record<number, AudioChangerTypeParam> = {
0: 'NONE',
1: 'CHILD',
2: 'LITTLE_GIRL',
3: 'MAN',
4: 'HEAVY_METAL',
5: 'COLD',
6: 'FOREIGNER',
7: 'TRAPPED_BEAST',
8: 'FATSO',
9: 'STRONG_CURRENT',
10: 'HEAVY_MACHINERY',
11: 'ETHEREAL',
} as const;
/**
* 混响类型映射表
* @internal
*/
const REVERB_TYPE_MAP: Record<number, AudioReverbTypeParam> = {
0: 'NONE',
1: 'KTV',
2: 'SMALL_ROOM',
3: 'AUDITORIUM',
4: 'DEEP',
5: 'LOUD',
6: 'METALLIC',
7: 'MAGNETIC',
} as const;
/**
* 耳返开关状态
* @type {Ref<boolean>}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { isEarMonitorOpened } = useAudioEffectState('your_live_id');
*
* // 监听耳返开关状态变化
* watch(isEarMonitorOpened, (newStatus) => {
* console.log('耳返开关状态:', newStatus);
* });
*
* // 获取当前耳返开关状态
* const isOpen = isEarMonitorOpened.value;
* console.log('当前耳返状态:', isOpen);
*/
const isEarMonitorOpened = ref<boolean>(false);
/**
* 耳返音量大小
* @type {Ref<number>}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { earMonitorVolume } = useAudioEffectState('your_live_id');
*
* // 监听耳返音量变化
* watch(earMonitorVolume, (newVolume) => {
* console.log('耳返音量:', newVolume);
* });
*
* // 获取当前耳返音量
* const volume = earMonitorVolume.value;
* console.log('当前耳返音量:', volume);
*/
const earMonitorVolume = ref<number>(0);
/**
* 变声状态
* @type {Ref<AudioChangerTypeParam>}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { audioChangerType } = useAudioEffectState('your_live_id');
*
* // 监听变声类型变化
* watch(audioChangerType, (newType) => {
* console.log('变声类型:', newType);
* });
*
* // 获取当前变声类型
* const type = audioChangerType.value;
* console.log('当前变声类型:', type);
*/
const audioChangerType = ref<AudioChangerTypeParam>('NONE');
/**
* 混响状态
* @type {Ref<AudioReverbTypeParam>}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { audioReverbType } = useAudioEffectState('your_live_id');
*
* // 监听混响类型变化
* watch(audioReverbType, (newType) => {
* console.log('混响类型:', newType);
* });
*
* // 获取当前混响类型
* const type = audioReverbType.value;
* console.log('当前混响类型:', type);
*/
const audioReverbType = ref<AudioReverbTypeParam>('NONE');
/**
* 设置变声效果
* @param {SetAudioChangerTypeOptions} params - 变声效果参数
* @returns {void}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { setAudioChangerType } = useAudioEffectState("your_live_id");
* setAudioChangerType({ changerType: 'MAN' });
*/
function setAudioChangerType(params: SetAudioChangerTypeOptions): void {
callUTSFunction("setAudioChangerType", params);
}
/**
* 设置混响效果
* @param {SetAudioReverbTypeOptions} params - 混响效果参数
* @returns {void}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { setAudioReverbType } = useAudioEffectState("your_live_id");
* setAudioReverbType({ reverbType: 'KTV' });
*/
function setAudioReverbType(params: SetAudioReverbTypeOptions): void {
callUTSFunction("setAudioReverbType", params);
}
/**
* 设置耳返开关状态
* @param {SetVoiceEarMonitorEnableOptions} params - 耳返开关参数
* @returns {void}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { setVoiceEarMonitorEnable } = useAudioEffectState("your_live_id");
* setVoiceEarMonitorEnable({ enable: true });
*/
function setVoiceEarMonitorEnable(params: SetVoiceEarMonitorEnableOptions): void {
callUTSFunction("setVoiceEarMonitorEnable", params);
}
/**
* 设置耳返音量大小
* @param {VolumeOptions} params - 耳返音量参数
* @returns {void}
* @memberof module:AudioEffectState
* @example
* import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState';
* const { setVoiceEarMonitorVolume } = useAudioEffectState("your_live_id");
* setVoiceEarMonitorVolume({ volume: 50 });
*/
function setVoiceEarMonitorVolume(params: VolumeOptions): void {
callUTSFunction("setVoiceEarMonitorVolume", params);
}
const onAudioEffectStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "isEarMonitorOpened") {
const data = safeJsonParse<boolean>(res, false);
isEarMonitorOpened.value = data;
} else if (eventName === "earMonitorVolume") {
const data = safeJsonParse<number>(res, 0);
earMonitorVolume.value = data;
} else if (eventName === "audioChangerType") {
const typeCode = safeJsonParse<number>(res, -1);
const type = mapChangerTypeCodeToChangerType(typeCode);
if (type) {
audioChangerType.value = type;
} else {
console.error(`Invalid changer type code received: ${typeCode}`);
}
} else if (eventName === "audioReverbType") {
const typeCode = safeJsonParse<number>(res, -1);
const type = mapReverbTypeCodeToReverbType(typeCode);
if (type) {
audioReverbType.value = type;
} else {
console.error(`Invalid reverb type code received: ${typeCode}`);
}
}
} catch (error) {
console.error("onAudioEffectStoreChanged error:", error);
}
};
function mapChangerTypeCodeToChangerType(typeCode: number): AudioChangerTypeParam | null {
const mappedType = CHANGER_TYPE_MAP[typeCode];
if (mappedType === undefined) {
console.warn(`Unknown changer type code: ${typeCode}`);
return null;
}
return mappedType;
}
function mapReverbTypeCodeToReverbType(typeCode: number): AudioReverbTypeParam | null {
const mappedType = REVERB_TYPE_MAP[typeCode];
if (mappedType === undefined) {
console.warn(`Unknown reverb type code: ${typeCode}`);
return null;
}
return mappedType;
}
function bindEvent(liveID: string): void {
getRTCRoomEngineManager().on("audioEffectStoreChanged", onAudioEffectStoreChanged, liveID);
}
export function useAudioEffectState(liveID: string) {
bindEvent(liveID);
return {
audioChangerType, // 变声状态
audioReverbType, // 混响状态
isEarMonitorOpened, // 耳返开关状态
earMonitorVolume, // 耳返音量大小
setAudioChangerType, // 设置变声效果
setAudioReverbType, // 设置混响效果
setVoiceEarMonitorEnable, // 设置耳返开关
setVoiceEarMonitorVolume, // 设置耳返音量
};
}
export default useAudioEffectState;

View File

@@ -0,0 +1,136 @@
/**
* @module BarrageState
* 弹幕管理管理模块
* @module_description
* 核心功能:处理直播间内的文本消息、自定义消息等弹幕功能,支持弹幕发送、消息状态同步等。
* 技术特点:支持高并发消息处理、实时消息同步、消息过滤、表情包支持等高级功能。
* 业务价值:为直播平台提供核心的互动能力,增强用户参与度和直播氛围。
* 应用场景:弹幕互动、消息管理、表情包、聊天室等社交互动场景。
*/
import { ref } from "vue";
import {
SendTextMessageOptions, SendCustomMessageOptions, BarrageParam, AppendLocalTipOptions
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 当前房间的弹幕消息列表
* @type {Ref<BarrageParam[]>}
* @memberof module:BarrageState
* @example
* import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
* const { messageList } = useBarrageState('your_live_id');
*
* // 监听弹幕消息列表变化
* watch(messageList, (newMessages) => {
* if (newMessages && newMessages.length > 0) {
* console.log('弹幕消息列表更新:', newMessages);
* newMessages.forEach(msg => {
* console.log('消息内容:', msg.content);
* console.log('发送者:', msg.sender);
* });
* }
* });
*
* // 获取当前弹幕列表
* const messages = messageList.value;
* console.log('当前弹幕数量:', messages.length);
*/
const messageList = ref<BarrageParam[]>([]);
/**
* 是否允许发送消息
* @type {Ref<boolean>}
* @memberof module:BarrageState
* @example
* import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
* const { allowSendMessage } = useBarrageState('your_live_id');
*
* // 监听消息发送权限变化
* watch(allowSendMessage, (newAllow) => {
* console.log('是否允许发送消息:', newAllow);
* });
*
* // 检查当前是否允许发送消息
* const allowSend = allowSendMessage.value;
* if (allowSend) {
* console.log('已启用消息发送功能');
* }
*/
const allowSendMessage = ref<boolean>(false);
/**
* 发送文本类型弹幕。
* @param {SendTextMessageOptions} params - 发送文本弹幕参数
* @returns {void}
* @memberof module:BarrageState
* @example
* import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
* const { sendTextMessage } = useBarrageState('your_live_id');
* sendTextMessage({ liveID: "your_live_id", text: 'Hello World' });
*/
function sendTextMessage(params : SendTextMessageOptions) : void {
callUTSFunction("sendTextMessage", params);
}
/**
* 添加本地提示消息。
* @param {AppendLocalTipOptions} params - 添加本地提示消息参数
* @returns {void}
* @memberof module:BarrageState
* @example
* import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
* const { appendLocalTip } = useBarrageState('your_live_id');
* appendLocalTip({ liveID: "your_live_id", message: { text: 'Hello World' } });
*/
function appendLocalTip(params : AppendLocalTipOptions) : void {
getRTCRoomEngineManager()["appendLocalTip"](params);
}
/**
* 发送自定义类型弹幕。
* @param {SendCustomMessageOptions} params - 发送自定义类型弹幕参数
* @returns {void}
* @memberof module:BarrageState
* @example
* import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
* const { sendCustomMessage } = useBarrageState('your_live_id');
* sendCustomMessage({ liveID: "your_live_id", businessID: "livekit", data: JSON.stringify("my custom message"});
*/
function sendCustomMessage(params : SendCustomMessageOptions) : void {
callUTSFunction("sendCustomMessage", params);
}
const onBarrageStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "messageList") {
const data = safeJsonParse<BarrageParam[]>(res, []);
messageList.value = data;
} else if (eventName === "allowSendMessage") {
const data = safeJsonParse<boolean>(res, false);
allowSendMessage.value = data;
}
} catch (error) {
console.error("onBarrageStoreChanged JSON parse error:", error);
}
};
function bindEvent(liveID : string) {
getRTCRoomEngineManager().on("barrageStoreChanged", onBarrageStoreChanged, liveID);
}
export function useBarrageState(liveID : string) {
bindEvent(liveID);
return {
messageList, // 当前房间的弹幕消息列表
// allowSendMessage, // 是否允许发送消息 TODO待支持
sendTextMessage, // 发送文本消息方法
sendCustomMessage, // 发送自定义消息方法
appendLocalTip // 添加本地提示消息方法
};
}
export default useBarrageState;

View File

@@ -0,0 +1,173 @@
/**
* @module BaseBeautyState
* @module_description
* 基础美颜管理模块
* 核心功能:提供磨皮、美白、红润等基础美颜效果调节,支持实时美颜参数调整。
* 技术特点:支持实时美颜处理、参数平滑调节、性能优化等高级技术。
* 业务价值:为直播平台提供基础的美颜能力,提升用户形象和直播质量。
* 应用场景:美颜直播、形象优化、美颜调节、直播美化等需要美颜功能的场景。
*/
import { ref } from "vue";
import { SetSmoothLevelOptions, SetWhitenessLevelOptions, SetRuddyLevelOptions } from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 磨皮级别 取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @type {Ref<number>}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { smoothLevel } = useBaseBeautyState('your_live_id');
*
* // 监听磨皮级别变化
* watch(smoothLevel, (newLevel) => {
* console.log('磨皮级别:', newLevel);
* });
*
* // 获取当前磨皮级别
* const level = smoothLevel.value;
* console.log('当前磨皮级别:', level);
*/
const smoothLevel = ref<number>(0);
/**
* 美白级别 取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @type {Ref<number>}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { whitenessLevel } = useBaseBeautyState('your_live_id');
*
* // 监听美白级别变化
* watch(whitenessLevel, (newLevel) => {
* console.log('美白级别:', newLevel);
* });
*
* // 获取当前美白级别
* const level = whitenessLevel.value;
* console.log('当前美白级别:', level);
*/
const whitenessLevel = ref<number>(0);
/**
* 红润级别 取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @type {Ref<number>}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { ruddyLevel } = useBaseBeautyState('your_live_id');
*
* // 监听红润级别变化
* watch(ruddyLevel, (newLevel) => {
* console.log('红润级别:', newLevel);
* });
*
* // 获取当前红润级别
* const level = ruddyLevel.value;
* console.log('当前红润级别:', level);
*/
const ruddyLevel = ref<number>(0);
const realUiValues = ref({
whiteness: 0,
smooth: 0,
ruddy: 0
});
/**
* 设置磨皮级别
* @param {SetSmoothLevelOptions} params - 磨皮参数,取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @returns {void}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { setSmoothLevel } = useBaseBeautyState('your_live_id');
* setSmoothLevel({ smoothLevel: 5 });
*/
function setSmoothLevel(params: SetSmoothLevelOptions): void {
callUTSFunction("setSmoothLevel", params);
}
/**
* 设置美白级别
* @param {SetWhitenessLevelOptions} params - 美白参数,取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @returns {void}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { setWhitenessLevel } = useBaseBeautyState('your_live_id');
* setWhitenessLevel({ whitenessLevel: 6 });
*/
function setWhitenessLevel(params: SetWhitenessLevelOptions): void {
callUTSFunction("setWhitenessLevel", params);
}
/**
* 设置红润级别
* @param {SetRuddyLevelOptions} params - 红润参数,取值范围[0,9]: 0 表示关闭9 表示效果最明显
* @returns {void}
* @memberof module:BaseBeautyState
* @example
* import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState';
* const { setRuddyLevel } = useBaseBeautyState('your_live_id');
* setRuddyLevel({ ruddyLevel: 4 });
*/
function setRuddyLevel(params: SetRuddyLevelOptions): void {
callUTSFunction("setRuddyLevel", params);
}
function setRealUiValue(type: 'whiteness' | 'smooth' | 'ruddy', value: number): void {
realUiValues.value[type] = value;
}
function getRealUiValue(type: 'whiteness' | 'smooth' | 'ruddy'): number {
return realUiValues.value[type];
}
function resetRealUiValues(): void {
realUiValues.value.whiteness = 0;
realUiValues.value.smooth = 0;
realUiValues.value.ruddy = 0;
}
const onBeautyStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "smoothLevel") {
const data = safeJsonParse<number>(res, 0);
smoothLevel.value = data;
} else if (eventName === "whitenessLevel") {
const data = safeJsonParse<number>(res, 0);
whitenessLevel.value = data;
} else if (eventName === "ruddyLevel") {
const data = safeJsonParse<number>(res, 0);
ruddyLevel.value = data;
}
} catch (error) {
console.error("onBeautyStoreChanged error:", error);
}
};
function bindEvent(liveID: string): void {
getRTCRoomEngineManager().on("beautyStoreChanged", onBeautyStoreChanged, liveID);
}
export function useBaseBeautyState(liveID: string) {
bindEvent(liveID);
return {
smoothLevel, // 磨皮级别状态
whitenessLevel, // 美白级别状态
ruddyLevel, // 红润级别状态
setSmoothLevel, // 设置磨皮级别方法
setWhitenessLevel, // 设置美白级别方法
setRuddyLevel, // 设置红润级别方法
realUiValues,
setRealUiValue,
getRealUiValue,
resetRealUiValues,
};
}
export default useBaseBeautyState;

View File

@@ -0,0 +1,274 @@
/**
* @module BattleState
* @module_description
* 直播 PK 管理模块
* 核心功能处理主播间的PK对战流程包括PK请求、接受、拒绝、退出等完整的PK管理功能。
* 技术特点支持实时PK状态同步、分数统计、PK时长控制、结果计算等高级功能。
* 业务价值:为直播平台提供丰富的互动玩法,增加主播收益和用户粘性。
* 应用场景主播PK、对战直播、分数统计、互动游戏等娱乐互动场景。
*/
import { ref } from "vue";
import { ILiveListener, RequestBattleOptions, CancelBattleRequestOptions, AcceptBattleOptions, RejectBattleOptions, ExitBattleOptions, BattleInfoParam, SeatUserInfoParam } from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 当前 PK 信息
* @type {Ref<BattleInfoParam | null>}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { currentBattleInfo } = useBattleState('your_live_id');
*
* // 监听当前 PK 信息变化
* watch(currentBattleInfo, (newBattle) => {
* if (newBattle) {
* console.log(' PK 已开始:', newBattle.battleID);
* }
* });
*
* // 获取当前 PK 信息
* const battle = currentBattleInfo.value;
*/
const currentBattleInfo = ref<BattleInfoParam | null>(null);
/**
* PK 用户列表
* @type {Ref<SeatUserInfoParam[]>}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { battleUsers } = useBattleState('your_live_id');
*
* // 监听当前 PK 用户列表变化
* watch(battleUsers, (newUsers) => {
* console.log('PK 用户列表更新:', newUsers);
* });
*
* // 获取当前 PK 用户列表
* const users = battleUsers.value;
* console.log('PK 用户列表更新:', users);
*/
const battleUsers = ref<SeatUserInfoParam[]>([]);
/**
* PK 分数映射
* @type {Ref<Map<string, number>> | null}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { battleScore } = useBattleState('your_live_id');
*
* // 监听当前 PK 分数变化
* watch(battleScore, (newScore) => {
* console.log('PK 分数更新:', newScore);
* });
*
* // 获取当前 PK 分数
* const score = battleScore.value;
* console.log('当前 PK 分数:', score);
*/
const battleScore = ref<Map<string, number> | null>(null);
/**
* 请求 PK
* @param {RequestBattleOptions} params - 请求 PK 参数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { requestBattle } = useBattleState("your_live_id");
* requestBattle({
* liveID: "your_live_id",
* userIDList: ["target_user_id"],
* timeout: 10,
* config: {
* duration: 300,
* needResponse: true,
* extensionInfo: "{"\"type\":\"standard\""}"
* },
* success: (battleInfo, result) => {
* console.log(' PK 请求成功:', battleInfo, result);
* },
* fail: (code, desc) => {
* console.error(' PK 请求失败:', code, desc);
* }
* });
*/
function requestBattle(params : RequestBattleOptions) : void {
callUTSFunction("requestBattle", params);
}
/**
* 取消 PK 请求
* @param {CancelBattleRequestOptions} params - 取消 PK 请求参数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { cancelBattleRequest } = useBattleState("your_live_id");
* cancelBattleRequest({
* liveID: "your_live_id",
* battleID: "battle_id",
* userIDList: ["target_user_id"],
* success: () => {
* console.log('取消 PK 请求成功');
* },
* fail: (code, desc) => {
* console.error('取消 PK 请求失败:', code, desc);
* }
* });
*/
function cancelBattleRequest(params : CancelBattleRequestOptions) : void {
callUTSFunction("cancelBattleRequest", params);
}
/**
* 接受 PK
* @param {AcceptBattleOptions} params - 接受 PK 参数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { acceptBattle } = useBattleState("your_live_id");
* acceptBattle({
* liveID: "your_live_id",
* battleID: "battle_id",
* success: () => {
* console.log('接受 PK 成功');
* },
* fail: (code, desc) => {
* console.error('接受 PK 失败:', code, desc);
* }
* });
*/
function acceptBattle(params : AcceptBattleOptions) : void {
callUTSFunction("acceptBattle", params);
}
/**
* 拒绝 PK
* @param {RejectBattleOptions} params - 拒绝 PK 参数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { rejectBattle } = useBattleState("your_live_id");
* rejectBattle({
* liveID: "your_live_id",
* battleID: "battle_id",
* success: () => {
* console.log('拒绝 PK 成功');
* },
* fail: (code, desc) => {
* console.error('拒绝 PK 失败:', code, desc);
* }
* });
*/
function rejectBattle(params : RejectBattleOptions) : void {
callUTSFunction("rejectBattle", params);
}
/**
* 退出 PK
* @param {ExitBattleOptions} params - 退出 PK 参数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { exitBattle } = useBattleState("your_live_id");
* exitBattle({
* liveID: "your_live_id",
* battleID: "battle_id",
* success: () => {
* console.log('退出 PK 成功');
* },
* fail: (code, desc) => {
* console.error('退出 PK 失败:', code, desc);
* }
* });
*/
function exitBattle(params : ExitBattleOptions) : void {
callUTSFunction("exitBattle", params);
}
/**
* 添加 PK 事件监听器
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onBattleStarted'( PK 开始)<br>'onBattleEnded'( PK 结束)<br>'onUserJoinBattle'(当前有用户加入 PK 对战)<br>'onUserExitBattle'(当前有用户退出 PK 对战)<br>'onBattleRequestReceived'(收到 PK 请求)<br>'onBattleRequestCancelled'(取消 PK 请求)<br>'onBattleRequestTimeout'(当前 PK 对战请求超时)<br>'onBattleRequestAccept'(当前 PK 对战请求被接受)<br>'onBattleRequestReject'(当前 PK 对战请求被拒绝)
* @param {ILiveListener} listener - 事件处理函数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { addBattleListener } = useBattleState('your_live_id');
* addBattleListener('your_live_id', 'onBattleStarted', {
* callback: (params) => {
* console.log(' PK 已开始:', params);
* }
* });
*/
function addBattleListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().addBattleListener(liveID, eventName, listener);
}
/**
* 移除 PK 事件监听器
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onBattleStarted'( PK 开始)<br>'onBattleEnded'( PK 结束)<br>'onUserJoinBattle'(当前有用户加入 PK 对战)<br>'onUserExitBattle'(当前有用户退出 PK 对战)<br>'onBattleRequestReceived'(收到 PK 请求)<br>'onBattleRequestCancelled'(取消 PK 请求)<br>'onBattleRequestTimeout'(当前 PK 对战请求超时)<br>'onBattleRequestAccept'(当前 PK 对战请求被接受)<br>'onBattleRequestReject'(当前 PK 对战请求被拒绝)
* @param {ILiveListener} listener - 事件处理函数
* @returns {void}
* @memberof module:BattleState
* @example
* import { useBattleState } from '@/uni_modules/tuikit-atomic-x/state/BattleState';
* const { removeBattleListener } = useBattleState('your_live_id');
* removeBattleListener('your_live_id', 'onBattleStarted', battleListener);
*/
function removeBattleListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().removeBattleListener(liveID, eventName, listener);
}
const onBattleStoreChanged = (eventName : string, res : string) : void => {
try {
switch (eventName) {
case "currentBattleInfo":
const battleData = safeJsonParse<BattleInfoParam | null>(res, null);
currentBattleInfo.value = battleData;
break;
case "battleUsers":
const requestsData = safeJsonParse<SeatUserInfoParam[]>(res, []);
battleUsers.value = requestsData;
break;
case "battleScore":
const scoreData = safeJsonParse<Map<string, number> | null>(res, null);
battleScore.value = scoreData;
break;
}
} catch (error) {
console.error("onBattleStoreChanged JSON parse error:", error);
}
};
function bindEvent(liveID : string) : void {
getRTCRoomEngineManager().on("battleStoreChanged", onBattleStoreChanged, liveID);
}
export function useBattleState(liveID : string) {
bindEvent(liveID);
return {
currentBattleInfo, // 当前 PK 信息
battleUsers, // PK 用户列表
battleScore, // PK 分数映射
requestBattle, // 请求 PK
cancelBattleRequest, // 取消 PK 请求
acceptBattle, // 接受 PK
rejectBattle, // 拒绝 PK
exitBattle, // 退出 PK
addBattleListener, // 添加 PK 事件监听
removeBattleListener // 移除 PK 事件监听
};
}
export default useBattleState;

View File

@@ -0,0 +1,367 @@
/**
* @module CoGuestState
* @module_description
* 直播连麦管理相关接口
* 核心功能:处理观众与主播之间的连麦互动,管理连麦申请、邀请、接受、拒绝等完整的连麦流程。
* 技术特点:基于音视频技术,支持连麦状态实时同步、音视频质量自适应、网络状况监控等高级功能。
* 业务价值:为直播平台提供观众参与互动的核心能力,增强用户粘性和直播趣味性。
* 应用场景观众连麦、互动问答、在线K歌、游戏直播等需要观众参与的互动场景。
*/
import { ref } from "vue";
import {
ApplyForSeatOptions, CancelApplicationOptions, AcceptApplicationOptions, RejectApplicationOptions,
InviteToSeatOptions, CancelInvitationOptions, AcceptInvitationOptions, RejectInvitationOptions, DisconnectOptions,
LiveUserInfoParam, SeatUserInfoParam, ILiveListener,
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 已连接的连麦嘉宾列表
* @type {Ref<SeatUserInfoParam[]>}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { connected } = useCoGuestState('your_live_id');
*
* // 监听已连接的连麦嘉宾列表变化
* watch(connected, (newConnected) => {
* if (newConnected && newConnected.length > 0) {
* console.log('连麦嘉宾列表更新:', newConnected);
* newConnected.forEach(guest => {
* console.log('嘉宾用户ID:', guest.userID);
* console.log('嘉宾昵称:', guest.nickname);
* });
* }
* });
*
* // 获取当前连麦嘉宾列表
* const guests = connected.value;
* console.log('当前连麦嘉宾数量:', guests.length);
*/
const connected = ref<SeatUserInfoParam[]>([]);
/**
* 被邀请上麦的用户列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { invitees } = useCoGuestState('your_live_id');
*
* // 监听被邀请用户列表变化
* watch(invitees, (newInvitees) => {
* if (newInvitees && newInvitees.length > 0) {
* console.log('被邀请用户列表更新:', newInvitees);
* newInvitees.forEach(user => {
* console.log('被邀请用户ID:', user.userID);
* console.log('被邀请用户昵称:', user.nickname);
* });
* }
* });
*
* // 获取当前被邀请用户列表
* const invitedUsers = invitees.value;
* console.log('当前被邀请用户数量:', invitedUsers.length);
*/
const invitees = ref<LiveUserInfoParam[]>([]);
/**
* 申请上麦的用户列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { applicants } = useCoGuestState('your_live_id');
*
* // 监听申请上麦用户列表变化
* watch(applicants, (newApplicants) => {
* if (newApplicants && newApplicants.length > 0) {
* console.log('申请上麦用户列表更新:', newApplicants);
* newApplicants.forEach(user => {
* console.log('申请用户ID:', user.userID);
* console.log('申请用户昵称:', user.nickname);
* });
* }
* });
*
* // 获取当前申请上麦用户列表
* const applyingUsers = applicants.value;
* console.log('当前申请用户数量:', applyingUsers.length);
*/
const applicants = ref<LiveUserInfoParam[]>([]);
/**
* 可邀请上麦的候选用户列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { candidates } = useCoGuestState('your_live_id');
*
* // 监听候选用户列表变化
* watch(candidates, (newCandidates) => {
* if (newCandidates && newCandidates.length > 0) {
* console.log('候选用户列表更新:', newCandidates);
* newCandidates.forEach(user => {
* console.log('候选用户ID:', user.userID);
* console.log('候选用户昵称:', user.nickname);
* });
* }
* });
*
* // 获取当前候选用户列表
* const candidateUsers = candidates.value;
* console.log('当前候选用户数量:', candidateUsers.length);
*/
const candidates = ref<LiveUserInfoParam[]>([]);
/**
* 申请连麦座位
* @param {ApplyForSeatOptions} params - 申请连麦座位参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { applyForSeat } = useCoGuestState("your_live_id");
* applyForSeat({ seatIndex: 2, timeout: 10 , extension: 'extra info'});
*/
function applyForSeat(params: ApplyForSeatOptions): void {
callUTSFunction("applyForSeat", params);
}
/**
* 取消申请
* @param {CancelApplicationOptions} params - 取消申请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { cancelApplication } = useCoGuestState("your_live_id");
* cancelApplication({});
*/
function cancelApplication(params: CancelApplicationOptions): void {
callUTSFunction("cancelApplication", params);
}
/**
* 接受申请
* @param {AcceptApplicationOptions} params - 接受申请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { acceptApplication } = useCoGuestState("your_live_id");
* acceptApplication({ userID: 'user123', seatIndex: 0 });
*/
function acceptApplication(params: AcceptApplicationOptions): void {
callUTSFunction("acceptApplication", params);
}
/**
* 拒绝申请
* @param {RejectApplicationOptions} params - 拒绝申请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { rejectApplication } = useCoGuestState("your_live_id");
* rejectApplication({ userID: 'user123' });
*/
function rejectApplication(params: RejectApplicationOptions): void {
callUTSFunction("rejectApplication", params);
}
/**
* 邀请上麦
* @param {InviteToSeatOptions} params - 邀请上麦参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { inviteToSeat } = useCoGuestState("your_live_id");
* inviteToSeat({ userID: 'user123', seatIndex: 2, timeout: 10 , extension: 'extra info'});
*/
function inviteToSeat(params: InviteToSeatOptions): void {
callUTSFunction("inviteToSeat", params);
}
/**
* 取消邀请
* @param {CancelInvitationOptions} params - 取消邀请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { cancelInvitation } = useCoGuestState("your_live_id");
* cancelInvitation({ inviteeID: 'user123' });
*/
function cancelInvitation(params: CancelInvitationOptions): void {
callUTSFunction("cancelInvitation", params);
}
/**
* 接受邀请
* @param {AcceptInvitationOptions} params - 接受邀请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { acceptInvitation } = useCoGuestState("your_live_id");
* acceptInvitation({ inviterID: 'user123' });
*/
function acceptInvitation(params: AcceptInvitationOptions): void {
callUTSFunction("acceptInvitation", params);
}
/**
* 拒绝邀请
* @param {RejectInvitationOptions} params - 拒绝邀请参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { rejectInvitation } = useCoGuestState("your_live_id");
* rejectInvitation({ inviterID: 'user123'});
*/
function rejectInvitation(params: RejectInvitationOptions): void {
callUTSFunction("rejectInvitation", params);
}
/**
* 断开连麦连接
* @param {DisconnectOptions} params - 断开连接参数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { disconnect } = useCoGuestState("your_live_id");
* disconnect();
*/
function disconnect(params: DisconnectOptions): void {
callUTSFunction("disconnect", params);
}
/**
* 添加连麦嘉宾侧事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onHostInvitationReceived'(收到主播邀请)<br>'onHostInvitationCancelled'(主播取消邀请)<br>'onGuestApplicationResponded'(嘉宾申请响应)<br>'onGuestApplicationNoResponse'(嘉宾申请无响应)<br>'onKickedOffSeat'(被踢下座位)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { addCoGuestGuestListener } = useCoGuestState("your_live_id");
* addCoGuestGuestListener('your_live_id', 'onHostInvitationReceived', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addCoGuestGuestListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().addCoGuestGuestListener(liveID, eventName, listener);
}
/**
* 移除连麦嘉宾侧事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onHostInvitationReceived'(收到主播邀请)<br>'onHostInvitationCancelled'(主播取消邀请)<br>'onGuestApplicationResponded'(嘉宾申请响应)<br>'onGuestApplicationNoResponse'(嘉宾申请无响应)<br>'onKickedOffSeat'(被踢下座位)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { removeCoGuestGuestListener } = useCoGuestState("your_live_id");
* removeCoGuestGuestListener('your_live_id', 'onHostInvitationReceived', guestListener);
*/
function removeCoGuestGuestListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().removeCoGuestGuestListener(liveID, eventName, listener);
}
/**
* 添加连麦主播侧事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onGuestApplicationReceived'(收到嘉宾申请)<br>'onGuestApplicationCancelled'(嘉宾取消申请)<br>'onGuestApplicationProcessedByOtherHost'(嘉宾申请被其他主播处理)<br>'onHostInvitationResponded'(主播邀请得到回应)<br>'onHostInvitationNoResponse'(主播邀请无响应)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { addCoGuestHostListener } = useCoGuestState("your_live_id");
* addCoGuestHostListener('your_live_id', 'onGuestApplicationReceived', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addCoGuestHostListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().addCoGuestHostListener(liveID, eventName, listener);
}
/**
* 移除连麦主播侧事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onGuestApplicationReceived'(收到嘉宾申请)<br>'onGuestApplicationCancelled'(嘉宾取消申请)<br>'onGuestApplicationProcessedByOtherHost'(嘉宾申请被其他主播处理)<br>'onHostInvitationResponded'(主播邀请得到回应)<br>'onHostInvitationNoResponse'(主播邀请无响应)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoGuestState
* @example
* import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
* const { removeCoGuestHostListener } = useCoGuestState("your_live_id");
* removeCoGuestHostListener('your_live_id', 'onGuestApplicationReceived', hostListener);
*/
function removeCoGuestHostListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().removeCoGuestHostListener(liveID, eventName, listener);
}
const onCoGuestStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "connected") {
const data = safeJsonParse<SeatUserInfoParam[]>(res, []);
connected.value = data;
} else if (eventName === "invitees") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
invitees.value = data;
} else if (eventName === "applicants") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
applicants.value = data;
} else if (eventName === "candidates") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
candidates.value = data;
}
} catch (error) {
console.error("onCoGuestStoreChanged error:", error);
}
};
function bindEvent(liveID: string): void {
getRTCRoomEngineManager().on("coGuestStoreChanged", onCoGuestStoreChanged, liveID);
}
export function useCoGuestState(liveID: string) {
bindEvent(liveID);
return {
connected, // 已连接的连麦嘉宾列表
invitees, // 被邀请上麦的用户列表
applicants, // 申请上麦的用户列表
candidates, // 可邀请上麦的候选用户列表
applyForSeat, // 申请连麦座位
cancelApplication, // 取消申请
acceptApplication, // 接受申请
rejectApplication, // 拒绝申请
inviteToSeat, // 邀请上麦
cancelInvitation, // 取消邀请
acceptInvitation, // 接受邀请
rejectInvitation, // 拒绝邀请
disconnect, // 断开连麦连接
addCoGuestGuestListener, // 添加嘉宾侧事件监听
removeCoGuestGuestListener,// 移除嘉宾侧事件监听
addCoGuestHostListener, // 添加主播侧事件监听
removeCoGuestHostListener, // 移除主播侧事件监听
};
}
export default useCoGuestState;

View File

@@ -0,0 +1,277 @@
/**
* @module CoHostState
* @module_description
* 连线主播管理模块
* 核心功能:实现主播间的连线功能,支持主播邀请、连线申请、连线状态管理等主播间互动功能。
* 技术特点:支持多主播音视频同步、画中画显示、音视频质量优化等高级技术,确保连线体验的流畅性。
* 业务价值为直播平台提供主播间协作的核心能力支持PK、合作直播等高级业务场景。
* 应用场景:主播连线、合作直播、跨平台连线、主播互动等高级直播场景。
*/
import { ref } from "vue";
import {
LiveUserInfoParam,
RequestHostConnectionOptions, CancelHostConnectionOptions, AcceptHostConnectionOptions,
RejectHostConnectionOptions, ExitHostConnectionOptions, ILiveListener
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 已连接的连线主播列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { connected } = useCoHostState('your_live_id');
*
* // 监听已连接的连线主播列表变化
* watch(connected, (newConnected) => {
* if (newConnected && newConnected.length > 0) {
* console.log('已连接的主播列表:', newConnected);
* }
* });
*
* // 获取当前已连接的连线主播数量
* const coHosts = connected.value;
* console.log('已连接的主播数:', coHosts.length);
*/
const connected = ref<LiveUserInfoParam[]>([]);
/**
* 被邀请连线的主播列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { invitees } = useCoHostState('your_live_id');
*
* // 监听被邀请的主播列表变化
* watch(invitees, (newInvitees) => {
* if (newInvitees && newInvitees.length > 0) {
* console.log('被邀请的主播列表:', newInvitees);
* }
* });
*
* // 获取当前被邀请的主播列表
* const invitedHosts = invitees.value;
* console.log('被邀请的主播数:', invitedHosts.length);
*/
const invitees = ref<LiveUserInfoParam[]>([]);
/**
* 当前申请连线的主播信息
* @type {Ref<LiveUserInfoParam | undefined>}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { applicant } = useCoHostState('your_live_id');
*
* // 监听申请连线的主播信息变化
* watch(applicant, (newApplicant) => {
* if (newApplicant) {
* console.log('申请主播:', newApplicant.userID);
* }
* });
*
* // 获取当前申请连线的主播信息
* const currentApplicant = applicant.value;
* if (currentApplicant) {
* console.log('当前申请连线的主播:', currentApplicant.nickname);
* }
*/
const applicant = ref<LiveUserInfoParam | undefined>();
/**
* 可邀请连线的候选主播列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { candidates } = useCoHostState('your_live_id');
*
* // 监听候选主播列表变化
* watch(candidates, (newCandidates) => {
* if (newCandidates && newCandidates.length > 0) {
* console.log('候选主播列表:', newCandidates);
* }
* });
*
* // 获取当前候选主播列表
* const candidateHosts = candidates.value;
* console.log('候选主播数:', candidateHosts.length);
*/
const candidates = ref<LiveUserInfoParam[]>([]);
/**
* 当前连线状态
* @type {Ref<string>}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { coHostStatus } = useCoHostState('your_live_id');
*
* // 监听连线状态变化
* watch(coHostStatus, (newStatus) => {
* console.log('连线状态:', newStatus);
* });
*
* // 获取当前连线状态
* const status = coHostStatus.value;
* console.log('当前连线状态:', status);
*/
const coHostStatus = ref<string>('')
/**
* 请求连线
* @param {RequestHostConnectionOptions} params - 请求连线参数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { requestHostConnection } = useCoHostState("your_live_id");
* requestHostConnection({});
*/
function requestHostConnection(params: RequestHostConnectionOptions): void {
callUTSFunction("requestHostConnection", params);
}
/**
* 取消连线请求
* @param {CancelHostConnectionOptions} params - 取消连线请求参数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { cancelHostConnection } = useCoHostState(“your_live_id”);
* cancelHostConnection({ toHostLiveID : "target_live_id" });
*/
function cancelHostConnection(params: CancelHostConnectionOptions): void {
callUTSFunction("cancelHostConnection", params);
}
/**
* 接受连线请求
* @param {AcceptHostConnectionOptions} params - 接受连线请求参数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { acceptHostConnection } = useCoHostState(“your_live_id”);
* acceptHostConnection({ fromHostLiveID: "from_live_id" });
*/
function acceptHostConnection(params: AcceptHostConnectionOptions): void {
callUTSFunction("acceptHostConnection", params);
}
/**
* 拒绝连线请求
* @param {RejectHostConnectionOptions} params - 拒绝连线请求参数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { rejectHostConnection } = useCoHostState(“your_live_id”);
* rejectHostConnection({ fromHostLiveID: "from_live_id" });
*/
function rejectHostConnection(params: RejectHostConnectionOptions): void {
callUTSFunction("rejectHostConnection", params);
}
/**
* 退出连线
* @param {ExitHostConnectionOptions} params - 退出连线参数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { exitHostConnection } = useCoHostState(“your_live_id”);
* exitHostConnection({});
*/
function exitHostConnection(params: ExitHostConnectionOptions): void {
callUTSFunction("exitHostConnection", params);
}
/**
* 添加连线主播事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onCoHostRequestReceived'(收到连线请求)<br>'onCoHostRequestCancelled'(连线请求被取消)<br>'onCoHostRequestAccepted'(连线请求被接受)<br>'onCoHostRequestRejected'(连线请求被拒绝)<br>'onCoHostRequestTimeout'(连线请求超时)<br>'onCoHostUserJoined'(连线用户加入)<br>'onCoHostUserLeft'(连线用户离开)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { addCoHostListener } = useCoHostState("your_live_id");
* addCoHostListener('your_live_id', 'onCoHostRequestReceived', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addCoHostListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().addCoHostListener(liveID, eventName, listener);
}
/**
* 移除连线主播事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onCoHostRequestReceived'(收到连线请求)<br>'onCoHostRequestCancelled'(连线请求被取消)<br>'onCoHostRequestAccepted'(连线请求被接受)<br>'onCoHostRequestRejected'(连线请求被拒绝)<br>'onCoHostRequestTimeout'(连线请求超时)<br>'onCoHostUserJoined'(连线用户加入)<br>'onCoHostUserLeft'(连线用户离开)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:CoHostState
* @example
* import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState';
* const { removeCoHostListener } = useCoHostState("your_live_id");
* removeCoHostListener('your_live_id', 'onCoHostRequestReceived', hostListener);
*/
function removeCoHostListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().removeCoHostListener(liveID, eventName, listener);
}
const onCoHostStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "connected") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
connected.value = data;
} else if (eventName === "invitees") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
invitees.value = data;
} else if (eventName === "applicant") {
const data = safeJsonParse<LiveUserInfoParam | null>(res, null);
applicant.value = data;
} else if (eventName === "candidates") {
const data = safeJsonParse<LiveUserInfoParam[]>(res, []);
candidates.value = data;
} else if (eventName === "coHostStatus") {
coHostStatus.value = JSON.parse(res);
}
} catch (error) {
console.error("onCoHostStoreChanged error:", error);
}
};
function bindEvent(liveID: string): void {
getRTCRoomEngineManager().on("coHostStoreChanged", onCoHostStoreChanged, liveID);
}
export function useCoHostState(liveID: string) {
bindEvent(liveID);
return {
coHostStatus, // 当前连线状态
connected, // 已连接的连线主播列表
invitees, // 被邀请连线的主播列表
applicant, // 当前申请连线的主播信息
// candidates, // 可邀请连线的候选主播列表: TODO待支持
requestHostConnection, // 请求连线
cancelHostConnection, // 取消连线请求
acceptHostConnection, // 接受连线请求
rejectHostConnection, // 拒绝连线请求
exitHostConnection, // 退出连线
addCoHostListener, // 添加连线事件监听
removeCoHostListener, // 移除连线事件监听
};
}
export default useCoHostState;

View File

@@ -0,0 +1,625 @@
/**
* @module DeviceState
* @module_description
* 设备状态管理模块
* 核心功能:管理摄像头、麦克风等音视频设备的控制,提供设备状态监控、权限检查等基础设备服务。
* 技术特点:支持多设备管理、设备状态实时监控、权限动态检查、设备故障自动恢复等高级功能。
* 业务价值:为直播系统提供稳定的设备基础,确保音视频采集的可靠性和用户体验。
* 应用场景:设备管理、权限控制、音视频采集、设备故障处理等基础技术场景。
*/
import { ref } from "vue";
import {
OpenLocalMicrophoneOptions, SetAudioRouteOptions, OpenLocalCameraOptions, SwitchCameraOptions,
UpdateVideoQualityOptions, SwitchMirrorOptions, VolumeOptions,
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import permission from "../utils/permission";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
export const DeviceStatusCode = {
OFF: 0,
ON: 1,
} as const;
export type DeviceStatusCodeType =
(typeof DeviceStatusCode)[keyof typeof DeviceStatusCode];
export const DeviceStatus = {
OFF: "OFF",
ON: "ON",
} as const;
export type DeviceStatusType = (typeof DeviceStatus)[keyof typeof DeviceStatus];
export const DeviceErrorCode = {
NO_ERROR: 0,
NO_DEVICE_DETECTED: 1,
NO_SYSTEM_PERMISSION: 2,
NOT_SUPPORT_CAPTURE: 3,
OCCUPIED_ERROR: 4,
UNKNOWN_ERROR: 5,
} as const;
export type DeviceErrorCodeType =
(typeof DeviceErrorCode)[keyof typeof DeviceErrorCode];
export const DeviceErrorEnum = {
NO_ERROR: "NO_ERROR",
NO_DEVICE_DETECTED: "NO_DEVICE_DETECTED",
NO_SYSTEM_PERMISSION: "NO_SYSTEM_PERMISSION",
NOT_SUPPORT_CAPTURE: "NOT_SUPPORT_CAPTURE",
OCCUPIED_ERROR: "OCCUPIED_ERROR",
UNKNOWN_ERROR: "UNKNOWN_ERROR",
} as const;
export type DeviceErrorType = (typeof DeviceErrorEnum)[keyof typeof DeviceErrorEnum];
export const AudioOutput = {
SPEAKERPHONE: "SPEAKERPHONE",
EARPIECE: "EARPIECE",
} as const;
export type AudioOutputType = (typeof AudioOutput)[keyof typeof AudioOutput];
const DEVICE_STATUS_MAP: Record<DeviceStatusCodeType, DeviceStatusType> = {
[DeviceStatusCode.OFF]: DeviceStatus.OFF,
[DeviceStatusCode.ON]: DeviceStatus.ON,
} as const;
const DEVICE_ERROR_MAP: Record<DeviceErrorCodeType, DeviceErrorType> = {
[DeviceErrorCode.NO_ERROR]: DeviceErrorEnum.NO_ERROR,
[DeviceErrorCode.NO_DEVICE_DETECTED]: DeviceErrorEnum.NO_DEVICE_DETECTED,
[DeviceErrorCode.NO_SYSTEM_PERMISSION]: DeviceErrorEnum.NO_SYSTEM_PERMISSION,
[DeviceErrorCode.NOT_SUPPORT_CAPTURE]: DeviceErrorEnum.NOT_SUPPORT_CAPTURE,
[DeviceErrorCode.OCCUPIED_ERROR]: DeviceErrorEnum.OCCUPIED_ERROR,
[DeviceErrorCode.UNKNOWN_ERROR]: DeviceErrorEnum.UNKNOWN_ERROR,
} as const;
/**
* 麦克风开启状态
* @type {Ref<DeviceStatusType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { microphoneStatus } = useDeviceState();
*
* // 监听麦克风状态变化
* watch(microphoneStatus, (newStatus) => {
* console.log('麦克风状态:', newStatus);
* if (newStatus === 'ON') {
* console.log('麦克风已打开');
* } else if (newStatus === 'OFF') {
* console.log('麦克风已关闭');
* }
* });
*/
const microphoneStatus = ref<DeviceStatusType>();
/**
* 麦克风最后一次错误状态
* @type {Ref<DeviceErrorType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { microphoneLastError } = useDeviceState();
*
* // 监听麦克风错误状态
* watch(microphoneLastError, (newError) => {
* if (newError && newError !== 'NO_ERROR') {
* console.log('麦克风错误:', newError);
* }
* });
*/
const microphoneLastError = ref<DeviceErrorType>();
/**
* 是否有音频发布权限
* @type {Ref<boolean>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { hasPublishAudioPermission } = useDeviceState();
*
* // 检查是否有音频发布权限
* const hasPermission = hasPublishAudioPermission.value;
* if (!hasPermission) {
* console.log('没有音频发布权限');
* }
*/
const hasPublishAudioPermission = ref<boolean>(true);
/**
* 采集音量大小0-100
* @type {Ref<number>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { captureVolume } = useDeviceState();
*
* // 监听采集音量变化
* watch(captureVolume, (newVolume) => {
* console.log('采集音量:', newVolume);
* });
*/
const captureVolume = ref<number>(0);
/**
* 当前麦克风音量0-100
* @type {Ref<number>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { currentMicVolume } = useDeviceState();
*
* // 监听麦克风音量变化
* watch(currentMicVolume, (newVolume) => {
* console.log('当前麦克风音量:', newVolume);
* });
*/
const currentMicVolume = ref<number>(0);
/**
* 输出音量大小0-100
* @type {Ref<number>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { outputVolume } = useDeviceState();
*
* // 监听输出音量变化
* watch(outputVolume, (newVolume) => {
* console.log('输出音量:', newVolume);
* });
*/
const outputVolume = ref<number>(0);
/**
* 摄像头开启状态
* @type {Ref<DeviceStatusType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { cameraStatus } = useDeviceState();
*
* // 监听摄像头状态变化
* watch(cameraStatus, (newStatus) => {
* console.log('摄像头状态:', newStatus);
* if (newStatus === 'ON') {
* console.log('摄像头已打开');
* }
* });
*/
const cameraStatus = ref<DeviceStatusType>();
/**
* 摄像头最后一次错误状态
* @type {Ref<DeviceErrorType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { cameraLastError } = useDeviceState();
*
* // 监听摄像头错误状态
* watch(cameraLastError, (newError) => {
* if (newError && newError !== 'NO_ERROR') {
* console.log('摄像头错误:', newError);
* }
* });
*/
const cameraLastError = ref<DeviceErrorType>();
/**
* 是否为前置摄像头
* @type {Ref<boolean>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { isFrontCamera } = useDeviceState();
*
* // 检查当前是否为前置摄像头
* const isFront = isFrontCamera.value;
* if (isFront) {
* console.log('当前使用前置摄像头');
* }
*/
const isFrontCamera = ref<boolean>();
/**
* 本地镜像类型
* @type {Ref<string>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { localMirrorType } = useDeviceState();
*
* // 获取本地镜像类型
* const mirrorType = localMirrorType.value;
* console.log('本地镜像类型:', mirrorType);
*/
const localMirrorType = ref<string>('');
/**
* 本地视频质量设置
* @type {Ref<any>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { localVideoQuality } = useDeviceState();
*
* // 获取本地视频质量设置
* const quality = localVideoQuality.value;
* console.log('本地视频质量:', quality);
*/
const localVideoQuality = ref<any>();
/**
* 当前音频输出路由(扬声器/耳机)
* @type {Ref<AudioOutputType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { currentAudioRoute } = useDeviceState();
*
* // 监听音频输出路由变化
* watch(currentAudioRoute, (newRoute) => {
* console.log('音频输出路由:', newRoute);
* if (newRoute === 'SPEAKERPHONE') {
* console.log('使用扬声器');
* } else if (newRoute === 'EARPIECE') {
* console.log('使用耳机');
* }
* });
*/
const currentAudioRoute = ref<AudioOutputType>();
/**
* 屏幕共享状态
* @type {Ref<DeviceStatusType>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { screenStatus } = useDeviceState();
*
* // 监听屏幕共享状态
* watch(screenStatus, (newStatus) => {
* console.log('屏幕共享状态:', newStatus);
* if (newStatus === 'ON') {
* console.log('屏幕共享已开启');
* }
* });
*/
const screenStatus = ref<DeviceStatusType>();
/**
* 网络信息状态
* @type {Ref<any>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { networkInfo } = useDeviceState();
*
* // 获取网络信息
* const info = networkInfo.value;
* console.log('网络信息:', info);
*/
const networkInfo = ref<any>();
/**
* @internal
*/
function mapStatusCodeToDeviceStatus(
statusCode: number
): DeviceStatusType | null {
const mappedStatus = DEVICE_STATUS_MAP[statusCode as DeviceStatusCodeType];
if (!mappedStatus) {
console.warn(`Unknown device status code: ${statusCode}`);
return null;
}
return mappedStatus;
}
/**
* @internal
*/
function mapErrorCodeToDeviceError(errorCode: number): DeviceErrorType | null {
const mappedError = DEVICE_ERROR_MAP[errorCode as DeviceErrorCodeType];
if (!mappedError) {
console.warn(`Unknown device error code: ${errorCode}`);
return null;
}
return mappedError;
}
/**
* 打开本地麦克风
* @param {OpenLocalMicrophoneOptions} [params] - 麦克风参数
* @returns {Promise<void>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { openLocalMicrophone } = useDeviceState();
* openLocalMicrophone({})
*/
async function openLocalMicrophone(params?: OpenLocalMicrophoneOptions): Promise<void> {
// @ts-ignore
if (uni.getSystemInfoSync().platform === "android") {
await permission.requestAndroidPermission(
"android.permission.RECORD_AUDIO"
);
}
callUTSFunction("openLocalMicrophone", params || {});
}
/**
* 关闭本地麦克风
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { closeLocalMicrophone } = useDeviceState();
* closeLocalMicrophone()
*/
function closeLocalMicrophone(): void {
callUTSFunction("closeLocalMicrophone");
}
/**
* 设置采集音量
* @param {VolumeOptions} params - 音量参数
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { setCaptureVolume } = useDeviceState();
* setCaptureVolume({ volume: 80 })
*/
function setCaptureVolume(params: VolumeOptions): void {
callUTSFunction("setCaptureVolume", params);
}
/**
* 设置输出音量
* @param {VolumeOptions} params - 音量参数
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { setOutputVolume } = useDeviceState();
* setOutputVolume({ volume: 90 })
*/
function setOutputVolume(params: VolumeOptions): void {
callUTSFunction("setOutputVolume", params);
}
/**
* 设置音频路由
* @param {SetAudioRouteOptions} params - 音频路由参数
* @returns {void}
* @memberof module:DeviceState
* @example
* // 设置为扬声器
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { setAudioRoute } = useDeviceState();
* setAudioRoute({ route: 'SPEAKERPHONE' })
*/
function setAudioRoute(params: SetAudioRouteOptions): void {
callUTSFunction("setAudioRoute", params);
}
/**
* 打开本地摄像头
* @param {OpenLocalCameraOptions} [params] - 摄像头参数
* @returns {Promise<void>}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { openLocalCamera } = useDeviceState();
* openLocalCamera({ isFront: true })
*/
async function openLocalCamera(params?: OpenLocalCameraOptions): Promise<void> {
// @ts-ignore
if (uni.getSystemInfoSync().platform === "android") {
await permission.requestAndroidPermission("android.permission.CAMERA");
}
callUTSFunction("openLocalCamera", params || {});
}
/**
* 关闭本地摄像头
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { closeLocalCamera } = useDeviceState();
* closeLocalCamera()
*/
function closeLocalCamera(): void {
callUTSFunction("closeLocalCamera");
}
/**
* 切换摄像头前后置
* @param {SwitchCameraOptions} params - 切换参数
* @returns {void}
* @memberof module:DeviceState
* @example
* // 切换到前置摄像头
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { switchCamera } = useDeviceState();
* switchCamera({ isFront: true })
*/
function switchCamera(params: SwitchCameraOptions): void {
callUTSFunction("switchCamera", params);
}
/**
* 切换镜像
* @param {SwitchMirrorOptions} params - 镜像参数
* @returns {void}
* @memberof module:DeviceState
* @example
* // 设置自动镜像
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { switchMirror } = useDeviceState();
* switchMirror({ mirrorType: 'AUTO' })
*/
function switchMirror(params: SwitchMirrorOptions): void {
callUTSFunction("switchMirror", params);
}
/**
* 更新视频质量
* @param {UpdateVideoQualityOptions} params - 视频质量参数
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { updateVideoQuality } = useDeviceState();
* updateVideoQuality({ quality: 'VIDEOQUALITY_1080P' })
*/
function updateVideoQuality(params: UpdateVideoQualityOptions): void {
callUTSFunction("updateVideoQuality", params);
}
/**
* 开始屏幕共享
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { startScreenShare } = useDeviceState();
* startScreenShare()
*/
function startScreenShare(): void {
callUTSFunction("startScreenShare");
}
/**
* 停止屏幕共享
* @returns {void}
* @memberof module:DeviceState
* @example
* import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
* const { stopScreenShare } = useDeviceState();
* stopScreenShare()
*/
function stopScreenShare(): void {
callUTSFunction("stopScreenShare");
}
const onDeviceStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "microphoneStatus") {
const statusCode = safeJsonParse<number>(res, -1);
const status = mapStatusCodeToDeviceStatus(statusCode);
if (status) {
microphoneStatus.value = status;
} else {
console.error(`Invalid microphone status code received: ${statusCode}`);
}
} else if (eventName === "microphoneLastError") {
const errorCode = safeJsonParse<number>(res, -1);
const error = mapErrorCodeToDeviceError(errorCode);
if (error) {
microphoneLastError.value = error;
} else {
console.error(`Invalid microphone error code received: ${errorCode}`);
}
} else if (eventName === "captureVolume") {
const data = safeJsonParse<number>(res, 0);
captureVolume.value = data;
} else if (eventName === "currentMicVolume") {
const data = safeJsonParse<number>(res, 0);
currentMicVolume.value = data;
}
else if (eventName === "outputVolume") {
const data = safeJsonParse<number>(res, 0);
outputVolume.value = data;
}
else if (eventName === "cameraStatus") {
const statusCode = safeJsonParse<number>(res, -1);
const status = mapStatusCodeToDeviceStatus(statusCode);
if (status) {
cameraStatus.value = status;
} else {
console.error(`Invalid camera status code received: ${statusCode}`);
}
} else if (eventName === "cameraLastError") {
const errorCode = safeJsonParse<number>(res, -1);
const error = mapErrorCodeToDeviceError(errorCode);
if (error) {
cameraLastError.value = error;
} else {
console.error(`Invalid camera error code received: ${errorCode}`);
}
} else if (eventName === "isFrontCamera") {
const data = safeJsonParse<boolean>(res, true);
isFrontCamera.value = data;
} else if (eventName === "localMirrorType") {
localMirrorType.value = JSON.parse(res);
} else if (eventName === "localVideoQuality") {
const data = safeJsonParse<boolean>(res, false);
localVideoQuality.value = data;
}
else if (eventName === "currentAudioRoute") {
const data = safeJsonParse<AudioOutputType>(res, AudioOutput.SPEAKERPHONE);
currentAudioRoute.value = data;
} else if (eventName === "screenStatus") {
const statusCode = safeJsonParse<number>(res, -1);
const status = mapStatusCodeToDeviceStatus(statusCode);
if (status) {
screenStatus.value = status;
} else {
console.error(`Invalid screen status code received: ${statusCode}`);
}
} else if (eventName === "networkInfo") {
networkInfo.value = safeJsonParse<any>(res, {});
}
} catch (error) {
console.error("onDeviceStoreChanged error:", error);
}
};
function bindEvent(): void {
getRTCRoomEngineManager().on("deviceStoreChanged", onDeviceStoreChanged, "");
}
export function useDeviceState() {
bindEvent();
return {
microphoneStatus, // 麦克风开启状态
microphoneLastError, // 麦克风最后一次错误状态
hasPublishAudioPermission,// 是否有音频发布权限
captureVolume, // 采集音量大小
currentMicVolume, // 当前麦克风音量
outputVolume, // 输出音量大小
cameraStatus, // 摄像头开启状态
cameraLastError, // 摄像头最后一次错误状态
isFrontCamera, // 是否为前置摄像头
localMirrorType, // 本地镜像类型
localVideoQuality, // 本地视频质量设置
currentAudioRoute, // 当前音频输出路由
screenStatus, // 屏幕共享状态
networkInfo, // 网络信息状态
openLocalMicrophone, // 打开本地麦克风
closeLocalMicrophone, // 关闭本地麦克风
setCaptureVolume, // 设置采集音量
setOutputVolume, // 设置输出音量
setAudioRoute, // 设置音频路由
openLocalCamera, // 打开本地摄像头
closeLocalCamera, // 关闭本地摄像头
switchCamera, // 切换摄像头
switchMirror, // 切换镜像
updateVideoQuality, // 更新视频质量
startScreenShare, // 开始屏幕共享
stopScreenShare, // 停止屏幕共享
};
}
export default useDeviceState;

View File

@@ -0,0 +1,188 @@
/**
* @module GiftState
* @module_description
* 礼物系统管理模块
* 核心功能:处理礼物的发送、接收、礼物列表管理等功能,支持礼物分类、礼物动画、礼物统计等完整礼物经济系统。
* 技术特点:支持礼物动画渲染、礼物特效处理、礼物统计、礼物排行榜等高级功能。
* 业务价值:为直播平台提供核心的变现能力,支持礼物经济、虚拟货币等商业模式。
* 应用场景:礼物打赏、虚拟货币、礼物特效、礼物统计等商业化场景。
*/
import { ref } from "vue";
import { ILiveListener, RefreshUsableGiftsOptions, SendGiftOptions, SetLanguageOptions } from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 礼物参数类型定义
* @typedef {Object} GiftParam
* @property {String} giftID - 礼物唯一标识
* @property {String} name - 礼物名称
* @property {String} desc - 礼物描述
* @property {String} iconURL - 礼物图标URL
* @property {String} resourceURL - 礼物动画资源URL
* @property {number} level - 礼物等级
* @property {number} coins - 礼物价格(金币)
* @property {Map<String, String>} extensionInfo - 扩展信息
* @memberof module:GiftState
*/
export type GiftParam = {
giftID : String;
name : String;
desc : String;
iconURL : String;
resourceURL : String;
level : number;
coins : number;
extensionInfo : Map<String, String>;
};
/**
* 礼物分类参数类型定义
* @typedef {Object} GiftCategoryParam
* @property {string} [categoryID] - 分类ID
* @property {string} [name] - 分类名称
* @property {string} [desc] - 分类描述
* @property {Map<string, string>} [extensionInfo] - 扩展信息
* @property {GiftParam[]} [giftList] - 分类下的礼物列表
* @memberof module:GiftState
*/
export type GiftCategoryParam = {
categoryID ?: string;
name ?: string;
desc ?: string;
extensionInfo ?: Map<string, string>;
giftList ?: GiftParam[];
};
/**
* 可用礼物列表
* @type {Ref<GiftCategoryParam[]>}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { usableGifts } = useGiftState('your_live_id');
*
* // 监听可用礼物列表变化
* watch(usableGifts, (newGifts) => {
* if (newGifts && newGifts.length > 0) {
* console.log('可用礼物更新:', newGifts);
* newGifts.forEach(gift => {
* console.log('礼物ID:', gift.giftID);
* console.log('礼物名称:', gift.name);
* console.log('礼物价格:', gift.coins);
* });
* }
* });
*
* // 获取当前可用礼物列表
* const gifts = usableGifts.value;
* console.log('当前可用礼物数量:', gifts.length);
*/
const usableGifts = ref<GiftCategoryParam[]>([]);
/**
* 刷新可用礼物列表
* @param {RefreshUsableGiftsOptions} params - 刷新礼物列表参数
* @returns {void}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { refreshUsableGifts } = useGiftState("your_live_id");
* refreshUsableGifts({});
*/
function refreshUsableGifts(params : RefreshUsableGiftsOptions) : void {
callUTSFunction("refreshUsableGifts", params);
}
/**
* 发送礼物
* @param {SendGiftOptions} params - 发送礼物参数
* @returns {void}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { sendGift } = useGiftState("your_live_id")
* sendGift({ liveID: 'xxx', giftID: "gift001", count: 1 });
*/
function sendGift(params : SendGiftOptions) : void {
callUTSFunction("sendGift", params);
}
/**
* 设置礼物语言
* @param {SetLanguageOptions} params - 设置礼物语言参数
* @returns {void}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { setLanguage } = useGiftState("your_live_id")
* setLanguage({ liveID: 'xxx', language: "zh-CN",});
*/
function setLanguage(params : SetLanguageOptions) : void {
callUTSFunction("setLanguage", params);
}
/**
* 添加礼物事件监听器
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onReceiveGift'(收到礼物)
* @param {ILiveListener} listener - 事件监听器函数
* @returns {void}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { addGiftListener } = useGiftState("your_live_id")
* addGiftListener('your_live_id', 'onReceiveGift', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addGiftListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().addGiftListener(liveID, eventName, listener);
}
/**
* 移除礼物事件监听器
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onReceiveGift'(收到礼物)
* @param {ILiveListener} listener - 事件监听器函数
* @returns {void}
* @memberof module:GiftState
* @example
* import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState';
* const { removeGiftListener } = useGiftState("your_live_id")
* removeGiftListener('your_live_id', 'onReceiveGift', giftListener);
*/
function removeGiftListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().removeGiftListener(liveID, eventName, listener);
}
const onGiftStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "usableGifts") {
const data = safeJsonParse<GiftCategoryParam[]>(res, []);
usableGifts.value = data;
}
} catch (error) {
console.error("onGiftStoreChanged JSON parse error:", error);
}
};
function bindEvent(liveID : string) : void {
getRTCRoomEngineManager().on("giftStoreChanged", onGiftStoreChanged, liveID);
}
export function useGiftState(liveID : string) {
bindEvent(liveID);
return {
usableGifts, // 可用礼物列表
refreshUsableGifts, // 刷新可用礼物列表
sendGift, // 发送礼物
setLanguage, // 设置礼物语言
addGiftListener, // 添加礼物事件监听
removeGiftListener // 移除礼物事件监听
};
}
export default useGiftState;

View File

@@ -0,0 +1,108 @@
/**
* @module LikeState
* @module_description
* 点赞互动管理模块
* 核心功能:处理直播间的点赞功能,支持点赞发送、点赞统计、点赞事件监听等互动功能。
* 技术特点:支持高并发点赞处理、实时点赞统计、点赞动画效果、点赞排行榜等高级功能。
* 业务价值:为直播平台提供基础的互动能力,增强用户参与度和直播氛围。
* 应用场景:点赞互动、人气统计、互动效果、用户参与等基础互动场景。
*/
import { ref } from "vue";
import { ILiveListener, SendLikeOptions } from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 总点赞数量
* @type {Ref<number>}
* @memberof module:LikeState
* @example
* import { useLikeState } from '@/uni_modules/tuikit-atomic-x/state/LikeState';
* const { totalLikeCount } = useLikeState('your_live_id');
*
* // 监听总点赞数量变化
* watch(totalLikeCount, (newCount) => {
* console.log('总点赞数量:', newCount);
* });
*
* // 获取当前总点赞数量
* const likeCount = totalLikeCount.value;
* console.log('当前获赞数:', likeCount);
*/
const totalLikeCount = ref<number>(0);
/**
* 发送点赞
* @param {SendLikeOptions} params - 点赞参数
* @returns {void}
* @memberof module:LikeState
* @example
* import { useLikeState } from '@/uni_modules/tuikit-atomic-x/state/LikeState';
* const { sendLike } = useLikeState("your_live_id");
* sendLike({ count: 1 });
*/
function sendLike(params: SendLikeOptions): void {
callUTSFunction("sendLike", params);
}
/**
* 添加点赞事件监听
* @param {string} liveID - 直播ID
* @param {string} eventName - 事件名称,可选值: 'onReceiveLikesMessage'(收到点赞消息)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LikeState
* @example
* import { useLikeState } from '@/uni_modules/tuikit-atomic-x/state/LikeState';
* const { addLikeListener } = useLikeState("your_live_id");
* addLikeListener('your_live_id', 'onReceiveLikesMessage', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addLikeListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().addLikeListener(liveID, eventName, listener);
}
/**
* 移除点赞事件监听
* @param {string} liveID - 直播ID
* @param {string} eventName - 事件名称,可选值: 'onReceiveLikesMessage'(收到点赞消息)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LikeState
* @example
* import { useLikeState } from '@/uni_modules/tuikit-atomic-x/state/LikeState';
* const { removeLikeListener } = useLikeState("your_live_id");
* removeLikeListener('your_live_id', 'onReceiveLikesMessage', likeListener);
*/
function removeLikeListener(liveID: string, eventName: string, listener: ILiveListener): void {
getRTCRoomEngineManager().removeLikeListener(liveID, eventName, listener);
}
const onLikeStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "totalLikeCount") {
const data = safeJsonParse<number>(res, 0);
totalLikeCount.value = data;
}
} catch (error) {
console.error("onLikeStoreChanged JSON parse error:", error);
}
};
function bindEvent(liveID: string): void {
getRTCRoomEngineManager().on("likeStoreChanged", onLikeStoreChanged, liveID);
}
export function useLikeState(liveID: string) {
bindEvent(liveID);
return {
totalLikeCount, // 总点赞数量
sendLike, // 发送点赞
addLikeListener, // 添加点赞事件监听
removeLikeListener, // 移除点赞事件监听
};
}
export default useLikeState;

View File

@@ -0,0 +1,204 @@
/**
* @module LiveAudienceState
* @module_description
* 直播间观众状态管理模块
* 核心功能:管理直播间观众列表,提供观众权限控制、管理员设置等直播间秩序维护功能。
* 技术特点:支持实时观众列表更新、权限分级管理、批量操作等高级功能,确保直播间秩序和用户体验。
* 业务价值:为直播平台提供完整的观众管理解决方案,支持大规模观众场景下的秩序维护。
* 应用场景:观众管理、权限控制、直播间秩序维护、观众互动管理等核心业务场景。
*/
import { ref } from "vue";
import {
FetchAudienceListOptions, SetAdministratorOptions, RevokeAdministratorOptions, KickUserOutOfRoomOptions,
DisableSendMessageOptions, LiveUserInfoParam, ILiveListener
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 直播间观众列表
* @type {Ref<LiveUserInfoParam[]>}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { audienceList } = useLiveAudienceState('your_live_id');
*
* // 监听观众列表变化
* watch(audienceList, (newAudienceList) => {
* if (newAudienceList && newAudienceList.length > 0) {
* console.log('观众列表更新:', newAudienceList);
* newAudienceList.forEach(audience => {
* console.log('观众ID:', audience.userID);
* });
* }
* });
*
* // 获取当前观众列表
* const audiences = audienceList.value;
* console.log('当前观众数:', audiences.length);
*/
const audienceList = ref<LiveUserInfoParam[]>([]);
/**
* 直播间观众数量
* @type {Ref<number>}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { audienceCount } = useLiveAudienceState('your_live_id');
*
* // 监听观众数量变化
* watch(audienceCount, (newCount) => {
* console.log('观众数量更新:', newCount);
* // 当观众数量达到某个阈值时可以进行特殊处理
* if (newCount >= 100) {
* console.log('直播热度很高观众数超过100');
* }
* });
*
* // 获取当前观众数量
* const count = audienceCount.value;
* console.log('当前观众数量:', count);
*/
const audienceCount = ref<number>(0);
/**
* 获取直播间观众列表
* @param {FetchAudienceListOptions} [params] - 获取观众列表参数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { fetchAudienceList } = useLiveAudienceState("your_live_id");
* fetchAudienceList();
*/
function fetchAudienceList(params ?: FetchAudienceListOptions) : void {
callUTSFunction("fetchAudienceList", params || {});
}
/**
* 设置管理员
* @param {SetAdministratorOptions} params - 设置管理员参数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { setAdministrator } = useLiveAudienceState("your_live_id");
* setAdministrator({ userID: 'user123' });
*/
function setAdministrator(params : SetAdministratorOptions) : void {
callUTSFunction("setAdministrator", params);
}
/**
* 撤销管理员权限
* @param {RevokeAdministratorOptions} params - 撤销管理员参数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { revokeAdministrator } = useLiveAudienceState("your_live_id");
* revokeAdministrator({ userID: 'user123' });
*/
function revokeAdministrator(params : RevokeAdministratorOptions) : void {
callUTSFunction("revokeAdministrator", params);
}
/**
* 将用户踢出直播间
* @param {KickUserOutOfRoomOptions} params - 踢出用户参数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { kickUserOutOfRoom } = useLiveAudienceState("your_live_id");
* kickUserOutOfRoom({ userID: 'user123' });
*/
function kickUserOutOfRoom(params : KickUserOutOfRoomOptions) : void {
callUTSFunction("kickUserOutOfRoom", params);
}
/**
* 禁用用户发送消息
* @param {DisableSendMessageOptions} params - 禁用发送消息参数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { disableSendMessage } = useLiveAudienceState("your_live_id");
* disableSendMessage({ userID: 'user123', disable: true });
*/
function disableSendMessage(params : DisableSendMessageOptions) : void {
callUTSFunction("disableSendMessage", params);
}
/**
* 添加观众事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onAudienceJoined'(观众加入)<br>'onAudienceLeft'(观众离开)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { addAudienceListener } = useLiveAudienceState("your_live_id");
* addAudienceListener('your_live_id', 'onAudienceJoined', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addAudienceListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().addAudienceListener(liveID, eventName, listener);
}
/**
* 移除观众事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onAudienceJoined'(观众加入)<br>'onAudienceLeft'(观众离开)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LiveAudienceState
* @example
* import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
* const { removeAudienceListener } = useLiveAudienceState("your_live_id");
* removeAudienceListener('your_live_id', 'onAudienceJoined', audienceListener);
*/
function removeAudienceListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().removeAudienceListener(liveID, eventName, listener);
}
const onLiveAudienceStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "audienceList") {
audienceList.value = safeJsonParse<LiveUserInfoParam[]>(res, []);
} else if (eventName === "audienceCount") {
audienceCount.value = safeJsonParse<number>(res, 0);
}
} catch (error) {
console.error("onLiveAudienceStoreChanged error:", error);
}
};
function bindEvent(liveID : string) : void {
getRTCRoomEngineManager().on("liveAudienceStoreChanged", onLiveAudienceStoreChanged, liveID);
}
export function useLiveAudienceState(liveID : string) {
bindEvent(liveID);
return {
audienceList, // 直播间观众列表
audienceCount, // 直播间观众数量
fetchAudienceList, // 获取观众列表
setAdministrator, // 设置管理员
revokeAdministrator, // 撤销管理员权限
kickUserOutOfRoom, // 将用户踢出直播间
disableSendMessage, // 禁用用户发送消息
addAudienceListener, // 添加观众事件监听
removeAudienceListener, // 移除观众事件监听
};
}
export default useLiveAudienceState;

View File

@@ -0,0 +1,281 @@
/**
* @module LiveListState
* @module_description
* 直播列表状态管理模块
* 核心功能:管理直播间的完整生命周期,包括创建、加入、离开、结束等核心业务流程。
* 技术特点支持分页加载、实时状态同步、直播信息动态更新采用响应式数据管理确保UI与数据状态实时同步。
* 业务价值:为直播平台提供核心的直播间管理能力,支持大规模并发直播场景,是直播业务的基础设施。
* 应用场景:直播列表展示、直播间创建、直播状态管理、直播数据统计等核心业务场景。
*/
import { ref } from "vue";
import {
LiveInfoParam, FetchLiveListOptions, CreateLiveOptions, JoinLiveOptions, LeaveLiveOptions, EndLiveOptions, UpdateLiveInfoOptions, UpdateLiveMetaDataOptions, CallExperimentalAPIOptions, ILiveListener,
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 直播列表数据
* @type {Ref<LiveInfoParam[]>}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { liveList } = useLiveListState();
*
* // 监听直播列表变化
* watch(liveList, (newList) => {
* if (newList && newList.length > 0) {
* console.log('直播列表更新:', newList);
* newList.forEach(live => {
* console.log('直播ID:', live.liveID);
* console.log('直播标题:', live.title);
* });
* }
* });
*
* // 获取当前直播列表
* const currentList = liveList.value;
* console.log('当前直播数量:', currentList.length);
*/
const liveList = ref<LiveInfoParam[]>([]);
/**
* 直播列表游标,用于分页加载
* @type {Ref<string>}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { liveListCursor } = useLiveListState();
*
* // 监听游标变化用于分页加载
* watch(liveListCursor, (newCursor) => {
* console.log('直播列表游标更新:', newCursor);
* // 当游标更新时,可以获取下一页数据
* if (newCursor) {
* console.log('加载下一页直播列表');
* }
* });
*
* // 获取当前游标
* const cursor = liveListCursor.value;
* if (cursor) {
* console.log('当前分页游标:', cursor);
* }
*/
const liveListCursor = ref<string>("");
/**
* 当前直播信息
* @type {Ref<LiveInfoParam | null>}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { currentLive } = useLiveListState();
*
* // 监听当前直播信息变化
* watch(currentLive, (newLive) => {
* if (newLive) {
* console.log('当前直播信息更新:', newLive);
* console.log('直播ID:', newLive.liveID);
* console.log('直播标题:', newLive.title);
* console.log('观看人数:', newLive.viewerCount);
* }
* });
*
* // 获取当前直播信息
* const live = currentLive.value;
* if (live) {
* console.log('当前进入的直播:', live.title);
* }
*/
const currentLive = ref<LiveInfoParam | null>(null);
/**
* 获取直播列表
* @param {FetchLiveListOptions} params - 获取参数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { fetchLiveList } = useLiveListState();
* fetchLiveList({ cursor: "", count: 20 });
*/
function fetchLiveList(params : FetchLiveListOptions) : void {
callUTSFunction("fetchLiveList", params);
}
/**
* 创建直播间
* @param {CreateLiveOptions} params - 创建参数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { createLive } = useLiveListState();
* createLive({ title: 'my live', coverUrl: 'https://example.com/cover.jpg'});
*/
function createLive(params : CreateLiveOptions) : void {
callUTSFunction("createLive", params);
}
/**
* 加入直播间
* @param {JoinLiveOptions} params - 加入参数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { joinLive } = useLiveListState();
* joinLive({ liveID: 'host_live_id' });
*/
function joinLive(params : JoinLiveOptions) : void {
callUTSFunction("joinLive", params);
}
/**
* 离开直播间
* @param {LeaveLiveOptions} [params] - 离开参数(可选)
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { leaveLive } = useLiveListState();
* leaveLive();
*/
function leaveLive(params ?: LeaveLiveOptions) : void {
callUTSFunction("leaveLive", params || {});
}
/**
* 结束直播
* @param {EndLiveOptions} [params] - 结束参数(可选)
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { endLive } = useLiveListState();
* endLive();
*/
function endLive(params ?: EndLiveOptions) : void {
callUTSFunction("endLive", params || {});
}
/**
* 更新直播信息
* @param {UpdateLiveInfoOptions} params - 更新参数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { updateLiveInfo } = useLiveListState();
* updateLiveInfo({ liveID: 'your_live_id', title: 'new title' });
*/
function updateLiveInfo(params : UpdateLiveInfoOptions) : void {
callUTSFunction("updateLiveInfo", params);
}
/**
* 更新直播元数据
* @param {UpdateLiveMetaDataOptions} params - 更新直播元数据, 监听 currentLive 获取更新结果
* @returns {void}
* @memberof module:LiveListState
* @example
* import { updateLiveMetaData } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { updateLiveMetaData } = useLiveListState();
* updateLiveMetaData({metaData: '['key': 'value']' });
*/
function updateLiveMetaData(params : UpdateLiveMetaDataOptions) : void {
callUTSFunction("updateLiveMetaData", params);
}
function callExperimentalAPI(params : CallExperimentalAPIOptions) : void {
const defaultCallback = {
onResponse: (res ?: string) => {
console.log("onExperimentalAPIResponse: ", res);
},
};
const finalParams = {
...params,
onResponse: params.onResponse || defaultCallback.onResponse,
};
console.log("callExperimentalAPI", finalParams);
getRTCRoomEngineManager().callExperimentalAPI(finalParams);
}
/**
* 添加直播列表事件监听
* @param {string} eventName - 事件名称,可选值: 'onLiveEnded'(直播结束)<br>'onKickedOutOfLive'(被踢出直播间)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { addLiveListListener } = useLiveListState();
* addLiveListListener('onLiveEnded', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addLiveListListener(eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().addLiveListListener(eventName, listener);
}
/**
* 移除直播列表事件监听
* @param {string} eventName - 事件名称,可选值: 'onLiveEnded'(直播结束)<br>'onKickedOutOfLive'(被踢出直播间)
* @param {ILiveListener} listener - 事件回调函数
* @returns {void}
* @memberof module:LiveListState
* @example
* import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState';
* const { removeLiveListListener } = useLiveListState();
* removeLiveListListener('onLiveEnded', liveEndedListener);
*/
function removeLiveListListener(eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().removeLiveListListener(eventName, listener);
}
const onLiveStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "liveList") {
const data = safeJsonParse<LiveInfoParam[]>(res, []);
liveList.value = data;
} else if (eventName === "liveListCursor") {
const data = safeJsonParse<string>(res, "");
liveListCursor.value = data;
} else if (eventName === "currentLive") {
const data = safeJsonParse<LiveInfoParam | null>(res, null);
currentLive.value = data;
}
} catch (error) {
console.error("onLiveStoreChanged error:", error);
}
};
function bindEvent() : void {
getRTCRoomEngineManager().on("liveStoreChanged", onLiveStoreChanged, "");
}
export function useLiveListState() {
bindEvent();
return {
liveList, // 直播列表数据
liveListCursor, // 直播列表分页游标
currentLive, // 当前直播信息
fetchLiveList, // 获取直播列表
createLive, // 创建直播
joinLive, // 加入直播
leaveLive, // 离开直播
endLive, // 结束直播
updateLiveInfo, // 更新直播信息
updateLiveMetaData, // 更新直播元数据
callExperimentalAPI,
addLiveListListener, // 添加事件监听
removeLiveListListener, // 移除事件监听
};
}
export default useLiveListState;

View File

@@ -0,0 +1,439 @@
/**
* @module LiveSeatState
* @module_description
* 直播间麦位管理模块
* 核心功能:实现多人连麦场景下的座位控制,支持复杂的座位状态管理和音视频设备控制。
* 技术特点:基于音视频技术,支持多路音视频流管理,提供座位锁定、设备控制、权限管理等高级功能。
* 业务价值为多人互动直播提供核心技术支撑支持PK、连麦、多人游戏等丰富的互动场景。
* 应用场景多人连麦、主播PK、互动游戏、在线教育、会议直播等需要多人音视频互动的场景。
*/
import { ref } from "vue";
import {
TakeSeatOptions, LeaveSeatOptions, MuteMicrophoneOptions, UnmuteMicrophoneOptions, KickUserOutOfSeatOptions,
MoveUserToSeatOptions, UnlockSeatOptions, SeatUserInfoParam, LockSeatOptions,
OpenRemoteCameraOptions, CloseRemoteCameraOptions, OpenRemoteMicrophoneOptions, CloseRemoteMicrophoneOptions, ILiveListener,
} from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 区域信息参数类型定义
* @typedef {Object} RegionInfoParams
* @property {number} x X坐标位置
* @property {number} y Y坐标位置
* @property {number} w 宽度
* @property {number} h 高度
* @property {number} zorder 层级顺序
* @memberof module:LiveSeatState
*/
export type RegionInfoParams = {
x : number;
y : number;
w : number;
h : number;
zorder : number;
}
/**
* 直播画布参数类型定义
* @typedef {Object} LiveCanvasParams
* @property {number} w 画布宽度
* @property {number} h 画布高度
* @property {string} [background] 背景色(可选)
* @memberof module:LiveSeatState
*/
export type LiveCanvasParams = {
w : number;
h : number;
background ?: string;
}
/**
* 座位信息类型定义
* @typedef {Object} SeatInfo
* @property {number} index 座位索引
* @property {boolean} isLocked 是否锁定
* @property {SeatUserInfoParam} userInfo 座位上用户信息
* @property {RegionInfoParams} region 座位区域信息
* @memberof module:LiveSeatState
*/
export type SeatInfo = {
index : number;
isLocked : boolean;
userInfo : SeatUserInfoParam;
region : RegionInfoParams;
}
/**
* 座位列表
* @type {Ref<SeatInfo[]>}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { seatList } = useLiveSeatState('your_live_id');
*
* // 监听座位列表变化
* watch(seatList, (newSeatList) => {
* if (newSeatList && newSeatList.length > 0) {
* console.log('座位列表更新:', newSeatList);
* newSeatList.forEach(seat => {
* console.log('座位索引:', seat.index);
* console.log('座位是否锁定:', seat.isLocked);
* if (seat.userInfo) {
* console.log('座位上用户ID:', seat.userInfo.userID);
* }
* });
* }
* });
*
* // 获取当前座位列表
* const seats = seatList.value;
* console.log('当前座位数:', seats.length);
*/
const seatList = ref<SeatInfo[]>([]);
/**
* 画布信息
* @type {Ref<LiveCanvasParams | null>}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { canvas } = useLiveSeatState('your_live_id');
*
* // 监听画布信息变化
* watch(canvas, (newCanvas) => {
* if (newCanvas) {
* console.log('画布信息更新:', newCanvas);
* }
* });
*
* // 获取当前画布信息
* const currentCanvas = canvas.value;
* if (currentCanvas) {
* console.log('当前画布分辨率:', currentCanvas.w, 'x', currentCanvas.h);
* }
*/
const canvas = ref<LiveCanvasParams | null>(null);
/**
* 正在说话的用户列表
* @type {Ref<Map<string, number> | null>}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { speakingUsers } = useLiveSeatState('your_live_id');
*
* // 监听正在说话的用户列表变化
* watch(speakingUsers, (newSpeakingUsers) => {
* if (newSpeakingUsers && newSpeakingUsers.size > 0) {
* console.log('正在说话的用户更新');
* newSpeakingUsers.forEach((volume, userID) => {
* console.log('用户ID:', userID);
* console.log('音量:', volume);
* });
* }
* });
*
* // 获取当前正在说话的用户数量
* const users = speakingUsers.value;
* if (users) {
* console.log('当前说话的用户数:', users.size);
* }
*/
const speakingUsers = ref<Map<string, number> | null>(null);
/**
* 用户上麦
* @param {TakeSeatOptions} params - 上麦参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { takeSeat } = useLiveSeatState('your_live_id');
* takeSeat({
* seatIndex: 1,
* onSuccess: () => console.log('上麦成功'),
* onError: (error) => console.error('上麦失败:', error)
* });
*/
function takeSeat(params : TakeSeatOptions) : void {
callUTSFunction("takeSeat", params);
}
/**
* 用户下麦
* @param {LeaveSeatOptions} params - 下麦参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { leaveSeat } = useLiveSeatState('your_live_id');
* leaveSeat({
* onSuccess: () => console.log('下麦成功'),
* onError: (error) => console.error('下麦失败:', error)
* });
*/
function leaveSeat(params : LeaveSeatOptions) : void {
callUTSFunction("leaveSeat", params);
}
/**
* 静音麦克风
* @param {MuteMicrophoneOptions} params - 静音参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { muteMicrophone } = useLiveSeatState('your_live_id');
* muteMicrophone({
* onSuccess: () => console.log('麦克风静音成功'),
* onError: (error) => console.error('麦克风静音失败:', error)
* });
*/
function muteMicrophone(params : MuteMicrophoneOptions) : void {
callUTSFunction("muteMicrophone", params);
}
/**
* 取消静音麦克风
* @param {UnmuteMicrophoneOptions} params - 取消静音参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { unmuteMicrophone } = useLiveSeatState('your_live_id');
* unmuteMicrophone({
* onSuccess: () => console.log('麦克风取消静音成功'),
* onError: (error) => console.error('麦克风取消静音失败:', error)
* });
*/
function unmuteMicrophone(params : UnmuteMicrophoneOptions) : void {
callUTSFunction("unmuteMicrophone", params);
}
/**
* 将用户踢出座位
* @param {KickUserOutOfSeatOptions} params - 踢出参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { kickUserOutOfSeat } = useLiveSeatState('your_live_id');
* kickUserOutOfSeat({
* seatIndex: 1,
* onSuccess: () => console.log('踢出用户成功'),
* onError: (error) => console.error('踢出用户失败:', error)
* });
*/
function kickUserOutOfSeat(params : KickUserOutOfSeatOptions) : void {
callUTSFunction("kickUserOutOfSeat", params);
}
/**
* 移动用户到指定座位
* @param {MoveUserToSeatOptions} params - 移动参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { moveUserToSeat } = useLiveSeatState('your_live_id');
* moveUserToSeat({
* fromSeatIndex: 1,
* toSeatIndex: 3,
* onSuccess: () => console.log('用户移动成功'),
* onError: (error) => console.error('用户移动失败:', error)
* });
*/
function moveUserToSeat(params : MoveUserToSeatOptions) : void {
callUTSFunction("moveUserToSeat", params);
}
/**
* 锁定座位
* @param {LockSeatOptions} params - 锁定参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { lockSeat } = useLiveSeatState('your_live_id');
* lockSeat({
* seatIndex: 2,
* onSuccess: () => console.log('座位锁定成功'),
* onError: (error) => console.error('座位锁定失败:', error)
* });
*/
function lockSeat(params : LockSeatOptions) : void {
callUTSFunction("lockSeat", params);
}
/**
* 解锁座位
* @param {UnlockSeatOptions} params - 解锁参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { unlockSeat } = useLiveSeatState('your_live_id');
* unlockSeat({
* seatIndex: 2,
* onSuccess: () => console.log('座位解锁成功'),
* onError: (error) => console.error('座位解锁失败:', error)
* });
*/
function unlockSeat(params : UnlockSeatOptions) : void {
callUTSFunction("unlockSeat", params);
}
/**
* 开启远程摄像头
* @param {OpenRemoteCameraOptions} params - 开启摄像头参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { openRemoteCamera } = useLiveSeatState('your_live_id');
* openRemoteCamera({
* seatIndex: 1,
* onSuccess: () => console.log('远程摄像头开启成功'),
* onError: (error) => console.error('远程摄像头开启失败:', error)
* });
*/
function openRemoteCamera(params : OpenRemoteCameraOptions) : void {
callUTSFunction("openRemoteCamera", params);
}
/**
* 关闭远程摄像头
* @param {CloseRemoteCameraOptions} params - 关闭摄像头参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { closeRemoteCamera } = useLiveSeatState('your_live_id');
* closeRemoteCamera({
* seatIndex: 1,
* onSuccess: () => console.log('远程摄像头关闭成功'),
* onError: (error) => console.error('远程摄像头关闭失败:', error)
* });
*/
function closeRemoteCamera(params : CloseRemoteCameraOptions) : void {
callUTSFunction("closeRemoteCamera", params);
}
/**
* 开启远程麦克风
* @param {OpenRemoteMicrophoneOptions} params - 开启麦克风参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { openRemoteMicrophone } = useLiveSeatState('your_live_id');
* openRemoteMicrophone({
* seatIndex: 1,
* onSuccess: () => console.log('远程麦克风开启成功'),
* onError: (error) => console.error('远程麦克风开启失败:', error)
* });
*/
function openRemoteMicrophone(params : OpenRemoteMicrophoneOptions) : void {
callUTSFunction("openRemoteMicrophone", params);
}
/**
* 关闭远程麦克风
* @param {CloseRemoteMicrophoneOptions} params - 关闭麦克风参数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { closeRemoteMicrophone } = useLiveSeatState('your_live_id');
* closeRemoteMicrophone({
* seatIndex: 1,
* onSuccess: () => console.log('远程麦克风关闭成功'),
* onError: (error) => console.error('远程麦克风关闭失败:', error)
* });
*/
function closeRemoteMicrophone(params : CloseRemoteMicrophoneOptions) : void {
callUTSFunction("closeRemoteMicrophone", params);
}
/**
* 添加座位事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onLocalCameraOpenedByAdmin'(本地摄像头被管理员开启)<br>'onLocalCameraClosedByAdmin'(本地摄像头被管理员关闭)<br>'onLocalMicrophoneOpenedByAdmin'(本地麦克风被管理员开启)<br>'onLocalMicrophoneClosedByAdmin'(本地麦克风被管理员关闭)
* @param {ILiveListener} listener - 事件处理函数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { addLiveSeatEventListener } = useLiveSeatState('your_live_id');
* addLiveSeatEventListener('your_live_id', 'onLocalCameraOpenedByAdmin', {
* callback: (params) => {
* console.log('result:', params);
* }
* });
*/
function addLiveSeatEventListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().addLiveSeatEventListener(liveID, eventName, listener);
}
/**
* 移除座位事件监听
* @param {string} liveID - 直播间ID
* @param {string} eventName - 事件名称,可选值: 'onLocalCameraOpenedByAdmin'(本地摄像头被管理员开启)<br>'onLocalCameraClosedByAdmin'(本地摄像头被管理员关闭)<br>'onLocalMicrophoneOpenedByAdmin'(本地麦克风被管理员开启)<br>'onLocalMicrophoneClosedByAdmin'(本地麦克风被管理员关闭)
* @param {ILiveListener} listener - 事件处理函数
* @returns {void}
* @memberof module:LiveSeatState
* @example
* import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState';
* const { removeLiveSeatEventListener } = useLiveSeatState('your_live_id');
* removeLiveSeatEventListener('your_live_id', 'onLocalCameraOpenedByAdmin', seatListener);
*/
function removeLiveSeatEventListener(liveID : string, eventName : string, listener : ILiveListener) : void {
getRTCRoomEngineManager().removeLiveSeatEventListener(liveID, eventName, listener);
}
const onLiveSeatStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "seatList") {
seatList.value = safeJsonParse<SeatInfo[]>(res, []);
} else if (eventName === "canvas") {
canvas.value = safeJsonParse<LiveCanvasParams | null>(res, null);
} else if (eventName === "speakingUsers") {
speakingUsers.value = safeJsonParse<Map<string, number> | null>(res, null);
}
} catch (error) {
console.error("onLiveSeatStoreChanged error:", error);
}
};
function bindEvent(liveID : string) : void {
getRTCRoomEngineManager().on("liveSeatStoreChanged", onLiveSeatStoreChanged, liveID);
}
export function useLiveSeatState(liveID : string) {
bindEvent(liveID);
return {
seatList, // 座位列表
canvas, // 画布信息
speakingUsers, // 正在说话的用户列表
takeSeat, // 用户上麦
leaveSeat, // 用户下麦
muteMicrophone, // 静音麦克风
unmuteMicrophone, // 取消静音麦克风
kickUserOutOfSeat, // 将用户踢出座位
moveUserToSeat, // 移动用户到指定座位
lockSeat, // 锁定座位
unlockSeat, // 解锁座位
openRemoteCamera, // 开启远程摄像头
closeRemoteCamera, // 关闭远程摄像头
openRemoteMicrophone, // 开启远程麦克风
closeRemoteMicrophone, // 关闭远程麦克风
addLiveSeatEventListener, // 添加座位事件监听
removeLiveSeatEventListener, // 移除座位事件监听
};
}
export default useLiveSeatState;

View File

@@ -0,0 +1,58 @@
/**
* @module LiveSummaryState
* @module_description
* 统计信息状态管理模块
* 核心功能:统计和展示直播过程中的关键数据,包括观看人数、点赞数、礼物数等实时统计。
* 技术特点:支持实时数据采集、数据聚合、统计分析等功能,提供完整的直播数据视图。
* 业务价值:为直播平台提供数据分析能力,支持直播效果评估和优化改进。
* 应用场景:直播数据展示、主播分析、流量统计、商业数据报表等数据分析场景。
*/
import { ref } from "vue";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
/**
* 直播间统计信息
* @type {Ref<any>}
* @memberof module:LiveSummaryState
* @example
* import { useLiveSummaryState } from '@/uni_modules/tuikit-atomic-x/state/LiveSummaryState';
* const { summaryData } = useLiveSummaryState('your_live_id');
*
* // 监听统计数据变化
* watch(summaryData, (newData) => {
* if (newData) {
* console.log('直播统计数据更新:', newData);
* }
* });
*
* // 获取当前统计数据
* const data = summaryData.value;
* if (data) {
* console.log('当前直播统计数据:', data);
* }
*/
const summaryData = ref<any>();
const onLiveSummaryStoreChanged = (eventName : string, res : string) : void => {
try {
if (eventName === "summaryData") {
const data = JSON.parse(res);
summaryData.value = data;
}
} catch (error) {
console.error("onLiveSummaryStoreChanged error:", error);
}
};
function bindEvent(liveID : string) : void {
getRTCRoomEngineManager().on("liveSummaryStoreChanged", onLiveSummaryStoreChanged, liveID);
}
export function useLiveSummaryState(liveID : string) {
bindEvent(liveID);
return {
summaryData, // 直播间统计信息
};
}
export default useLiveSummaryState;

View File

@@ -0,0 +1,146 @@
/**
* @module LoginState
* @module_description
* 用户身份认证与登录管理模块
* 核心功能:负责用户身份验证、登录状态管理、用户信息维护等基础认证服务。
* 技术特点:支持多种认证方式、会话管理、权限验证等高级功能,确保用户身份的安全和有效。
* 业务价值:为直播平台提供基础的用户认证能力,是所有其他业务模块的前置条件。
* 应用场景:用户登录、身份验证、会话管理、权限控制等基础认证场景。
*/
import { ref } from "vue";
import { UserProfileParam, LoginOptions, LogoutOptions, SetSelfInfoOptions } from "@/uni_modules/tuikit-atomic-x";
import { getRTCRoomEngineManager } from "./rtcRoomEngine";
import { callUTSFunction, safeJsonParse } from "../utils/utsUtils";
/**
* 当前登录用户信息
* @type {Ref<UserProfileParam>}
* @memberof module:LoginState
* @example
* import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState';
* const { loginUserInfo } = useLoginState();
*
* // 监听用户信息变化
* watch(loginUserInfo, (newUserInfo) => {
* if (newUserInfo) {
* console.log('用户信息更新:', newUserInfo);
* console.log('用户ID:', newUserInfo.userID);
* console.log('用户昵称:', newUserInfo.nickname);
* console.log('用户头像:', newUserInfo.avatarURL);
* }
* });
*
* // 获取当前用户信息
* const currentUser = loginUserInfo.value;
* if (currentUser) {
* console.log('当前登录用户:', currentUser.nickname);
* }
*/
const loginUserInfo = ref<UserProfileParam>();
/**
* 当前登录状态
* @type {Ref<string>}
* @memberof module:LoginState
* @example
* import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState';
* const { logout } = useLoginState();
* logout({
* onSuccess: () => console.log('登出成功'),
* onError: (error) => console.error('登出失败:', error)
* });
*/
const loginStatus = ref<string>();
/**
* 登录方法
* @param {LoginOptions} params - 登录参数
* @returns {void}
* @memberof module:LoginState
* @example
* import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState';
* const { login } = useLoginState();
* login({
* sdkAppID: 1400000000,
* userID: 'user123',
* userSig: 'eJx1kF1PwzAMhv9KlG...',
* onSuccess: () => console.log('登录成功'),
* onError: (error) => console.error('登录失败:', error)
* });
*/
function login(params: LoginOptions): void {
callUTSFunction("login", params);
}
/**
* 登出方法
* @param {LogoutOptions} [params] - 登出参数(可选)
* @returns {void}
* @memberof module:LoginState
* @example
* import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState';
* const { logout } = useLoginState();
* logout({
* onSuccess: () => console.log('登出成功'),
* onError: (error) => console.error('登出失败:', error)
* });
*/
function logout(params?: LogoutOptions): void {
callUTSFunction("logout", params || {});
}
/**
* 设置用户信息
* @param {SetSelfInfoOptions} userInfo - 用户信息
* @returns {void}
* @memberof module:LoginState
* @example
* import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState';
* const { setSelfInfo } = useLoginState();
* setSelfInfo({
* userID: 'user123',
* nickname: '张三',
* avatarURL: 'https://example.com/avatar.jpg',
* onSuccess: () => console.log('用户信息设置成功'),
* onError: (error) => console.error('用户信息设置失败:', error)
* });
*/
function setSelfInfo(userInfo: SetSelfInfoOptions): void {
callUTSFunction("setSelfInfo", userInfo);
}
function getLoginUserInfo(): UserProfileParam | undefined {
return loginUserInfo.value;
}
const onLoginStoreChanged = (eventName: string, res: string): void => {
try {
if (eventName === "loginUserInfo") {
const data = safeJsonParse<UserProfileParam>(res, {});
loginUserInfo.value = data;
} else if (eventName === "loginStatus") {
loginStatus.value = safeJsonParse<string>(res, "");
}
} catch (error) {
console.error("onLoginStoreChanged error:", error);
}
};
function bindEvent(): void {
getRTCRoomEngineManager().on("loginStoreChanged", onLoginStoreChanged, '');
}
export function useLoginState() {
bindEvent();
return {
loginUserInfo, // 当前登录用户信息
loginStatus, // 当前登录状态
login, // 登录方法
logout, // 登出方法
setSelfInfo, // 设置用户信息
getLoginUserInfo, // 获取登录用户信息
};
}
export default useLoginState;

View File

@@ -0,0 +1,11 @@
import { RTCRoomEngineManager } from "@/uni_modules/tuikit-atomic-x";
let instance : RTCRoomEngineManager | null = null;
//TODO: 这个文件的命名待讨论,或者直接干掉这个类?
export function getRTCRoomEngineManager() : RTCRoomEngineManager {
if (!instance) {
instance = new RTCRoomEngineManager();
}
return instance;
}

View File

@@ -0,0 +1,286 @@
/**
* 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启
*/
declare const plus: any;
// #ifdef APP-PLUS
const isIos = plus.os.name === 'iOS';
// #endif
// 判断推送权限是否开启
function judgeIosPermissionPush() {
let result = false;
const UIApplication = plus.ios.import('UIApplication');
const app = UIApplication.sharedApplication();
let enabledTypes = 0;
if (app.currentUserNotificationSettings) {
const settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute('types');
console.log(`enabledTypes1:${enabledTypes}`);
if (enabledTypes == 0) {
console.log('推送权限没有开启');
} else {
result = true;
console.log('已经开启推送功能!');
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
console.log('推送权限没有开启!');
} else {
result = true;
console.log('已经开启推送功能!');
}
console.log(`enabledTypes2:${enabledTypes}`);
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
// 判断定位权限是否开启
function judgeIosPermissionLocation() {
let result = false;
const cllocationManger = plus.ios.import('CLLocationManager');
const status = cllocationManger.authorizationStatus();
result = status != 2;
console.log(`定位权限开启:${result}`);
// 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation
/* var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
console.log("enable:" + enable);
console.log("status:" + status);
if (enable && status != 2) {
result = true;
console.log("手机定位服务已开启且已授予定位权限");
} else {
console.log("手机系统的定位没有打开或未给予定位权限");
} */
plus.ios.deleteObject(cllocationManger);
return result;
}
// 判断麦克风权限是否开启
function judgeIosPermissionRecord() {
let result = false;
const avaudiosession = plus.ios.import('AVAudioSession');
const avaudio = avaudiosession.sharedInstance();
const permissionStatus = avaudio.recordPermission();
console.log(`permissionStatus:${permissionStatus}`);
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
console.log('麦克风权限没有开启');
} else {
result = true;
console.log('麦克风权限已经开启');
}
plus.ios.deleteObject(avaudiosession);
return result;
}
// 判断相机权限是否开启
function judgeIosPermissionCamera() {
let result = false;
const AVCaptureDevice = plus.ios.import('AVCaptureDevice');
const authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
console.log(`authStatus:${authStatus}`);
if (authStatus == 3) {
result = true;
console.log('相机权限已经开启');
} else {
console.log('相机权限没有开启');
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
// 判断相册权限是否开启
function judgeIosPermissionPhotoLibrary() {
let result = false;
const PHPhotoLibrary = plus.ios.import('PHPhotoLibrary');
const authStatus = PHPhotoLibrary.authorizationStatus();
console.log(`authStatus:${authStatus}`);
if (authStatus == 3) {
result = true;
console.log('相册权限已经开启');
} else {
console.log('相册权限没有开启');
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
// 判断通讯录权限是否开启
function judgeIosPermissionContact() {
let result = false;
const CNContactStore = plus.ios.import('CNContactStore');
const cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus == 3) {
result = true;
console.log('通讯录权限已经开启');
} else {
console.log('通讯录权限没有开启');
}
plus.ios.deleteObject(CNContactStore);
return result;
}
// 判断日历权限是否开启
function judgeIosPermissionCalendar() {
let result = false;
const EKEventStore = plus.ios.import('EKEventStore');
const ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = true;
console.log('日历权限已经开启');
} else {
console.log('日历权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// 判断备忘录权限是否开启
function judgeIosPermissionMemo() {
let result = false;
const EKEventStore = plus.ios.import('EKEventStore');
const ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = true;
console.log('备忘录权限已经开启');
} else {
console.log('备忘录权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// Android权限查询
function requestAndroidPermission(permissionID: string) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
(resultObj: any) => {
let result = 0;
for (let i = 0; i < resultObj.granted.length; i++) {
const grantedPermission = resultObj.granted[i];
console.log(`已获取的权限:${grantedPermission}`);
result = 1;
}
for (let i = 0; i < resultObj.deniedPresent.length; i++) {
const deniedPresentPermission = resultObj.deniedPresent[i];
console.log(`拒绝本次申请的权限:${deniedPresentPermission}`);
result = 0;
}
for (let i = 0; i < resultObj.deniedAlways.length; i++) {
const deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log(`永久拒绝申请的权限:${deniedAlwaysPermission}`);
result = -1;
}
resolve(result);
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
// if (result != 1) {
// gotoAppPermissionSetting()
// }
},
(error: any) => {
console.log(`申请权限错误:${error.code} = ${error.message}`);
resolve({
code: error.code,
message: error.message,
});
}
);
});
}
// 使用一个方法,根据参数判断权限
function judgeIosPermission(permissionID: string) {
if (permissionID == 'location') {
return judgeIosPermissionLocation();
}
if (permissionID == 'camera') {
return judgeIosPermissionCamera();
}
if (permissionID == 'photoLibrary') {
return judgeIosPermissionPhotoLibrary();
}
if (permissionID == 'record') {
return judgeIosPermissionRecord();
}
if (permissionID == 'push') {
return judgeIosPermissionPush();
}
if (permissionID == 'contact') {
return judgeIosPermissionContact();
}
if (permissionID == 'calendar') {
return judgeIosPermissionCalendar();
}
if (permissionID == 'memo') {
return judgeIosPermissionMemo();
}
return false;
}
// 跳转到**应用**的权限页面
function gotoAppPermissionSetting() {
if (isIos) {
const UIApplication = plus.ios.import('UIApplication');
const application2 = UIApplication.sharedApplication();
const NSURL2 = plus.ios.import('NSURL');
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
const setting2 = NSURL2.URLWithString('app-settings:');
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
// console.log(plus.device.vendor);
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const mainActivity = plus.android.runtimeMainActivity();
const intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
const uri = Uri.fromParts('package', mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
// 检查系统的设备服务是否开启
// var checkSystemEnableLocation = async function () {
function checkSystemEnableLocation() {
if (isIos) {
let result = false as any;
const cllocationManger = plus.ios.import('CLLocationManager');
result = cllocationManger.locationServicesEnabled();
console.log(`系统定位开启:${result}`);
plus.ios.deleteObject(cllocationManger);
return result;
}
const context = plus.android.importClass('android.content.Context');
const locationManager = plus.android.importClass(
'android.location.LocationManager'
);
const main = plus.android.runtimeMainActivity();
const mainSvr = main.getSystemService(context.LOCATION_SERVICE);
const result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);
console.log(`系统定位开启:${result}`);
return result;
}
// module.exports = {
// judgeIosPermission: judgeIosPermission,
// requestAndroidPermission: requestAndroidPermission,
// checkSystemEnableLocation: checkSystemEnableLocation,
// gotoAppPermissionSetting: gotoAppPermissionSetting
// }
// HBuilder 选择 vue3 时, 上面的打包无法通过 import 进行引入
export default {
judgeIosPermission,
requestAndroidPermission,
checkSystemEnableLocation,
gotoAppPermissionSetting,
};

View File

@@ -0,0 +1,29 @@
import { useLiveListState } from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
const LanguageMap = {
'zh-CN': 'zh-Hans', // android
'zh-TW': 'zh-Hant', // android
'zh-Hans-US': 'zh-Hans', // iOS
en: 'en',
};
const { callExperimentalAPI } = useLiveListState();
export function setSdkLanguageFromSystem() {
uni.$liveID = '';
uni.getSystemInfo()
.then((systemInfo) => {
console.log(`systemInfo.language: ${systemInfo.language}`);
const data = {
api: 'setCurrentLanguage',
params: {
language: LanguageMap[systemInfo.language] || LanguageMap['zh-CN'],
},
};
console.log(`callExperimentalAPI data: ${JSON.stringify(data)}`);
callExperimentalAPI({ jsonData: JSON.stringify(data) });
})
.catch((e) => {
console.error('获取系统信息失败', e);
});
}

View File

@@ -0,0 +1,64 @@
import { getRTCRoomEngineManager } from "../state/rtcRoomEngine";
/**
* 安全的JSON解析函数
* @param jsonString JSON字符串
* @param defaultValue 解析失败时的默认值
*/
export function safeJsonParse<T>(jsonString: string, defaultValue: T): T {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error("JSON parse error:", error);
return defaultValue;
}
}
/**
* 通用的UTS函数调用方法
* @param funcName UTS函数名
* @param args 函数参数包含success和fail callback直接传递对象
*/
export function callUTSFunction(funcName: string, args?: any): void {
const defaultCallback = {
success: (res?: string) => {
console.log(`[${funcName}] Success:`, {
funcName,
args: JSON.stringify(args),
result: res,
});
},
fail: (errCode?: number, errMsg?: string) => {
console.error(`[${funcName}] Failed:`, {
funcName,
args: JSON.stringify(args),
errCode,
errMsg,
});
},
};
let finalArgs = args || {};
// 如果args中没有callback则添加默认callback
if (!finalArgs.success && !finalArgs.fail) {
finalArgs = {
...finalArgs,
...defaultCallback,
};
}
// 直接调用UTS层
try {
console.log(`[${funcName}] Calling with args:`, finalArgs);
// 直接传递对象给UTS层
getRTCRoomEngineManager()[funcName](finalArgs);
} catch (error) {
console.error(`[${funcName}] Error calling UTS function:`, error);
// 如果有失败回调,调用它
if (finalArgs.fail) {
finalArgs.fail(-1, `Failed to call ${funcName}: ${error}`);
}
}
}

View File

@@ -0,0 +1,14 @@
{
"minSdkVersion": "21",
"dependencies": [
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3",
"com.google.code.gson:gson:2.10.1",
"com.github.yyued:SVGAPlayer-Android:2.6.1",
"androidx.constraintlayout:constraintlayout:2.1.4",
"com.google.android.material:material:1.11.0",
"com.tencent.liteav:LiteAVSDK_Professional:12.7+",
"com.tencent.imsdk:imsdk-plus:8.7.+",
"com.tencent.liteav.tuikit:tuicore:8.7.+",
"io.trtc.uikit:common:3.3.+"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
package uts.sdk.modules.atomicx.kotlin
import android.graphics.BitmapFactory
import android.text.TextUtils
import com.google.gson.Gson
import com.tencent.cloud.tuikit.engine.room.TUIRoomDefine
import com.tencent.cloud.tuikit.engine.room.TUIRoomEngine
private const val TAG = "UTS-CallExperimentalApi: "
object ExperimentalApiInvoker {
private val gson = Gson()
// const data = { "api": "setTestEnvironment", "params": { "enableRoomTestEnv": true } } // 设置 IM 测试环境
// const data = { "api": "setLocalVideoMuteImage", "params": { "image": "filePath" } } // 设置垫片
// const giftData = { "api": "setCurrentLanguage", "params": { "language" : "en"} } // 礼物功能设置语言
public fun callExperimentalAPI(
jsonString: String,
callback: TUIRoomDefine.ExperimentalAPIResponseCallback?,
) {
val requestData: RequestData = gson.fromJson(jsonString, RequestData::class.java)
if (requestData.api == "setLocalVideoMuteImage") {
setLocalVideoMuteImage(requestData, callback)
return
}
TUIRoomEngine.sharedInstance().callExperimentalAPI(jsonString) { jsonData ->
// Logger.i(TAG + "${requestData.api}: onResponse: $jsonData")
callback?.onResponse(jsonData)
}
}
private fun setLocalVideoMuteImage(
data: RequestData,
callback: TUIRoomDefine.ExperimentalAPIResponseCallback?,
) {
try {
val filePath = data.params?.image
if (TextUtils.isEmpty(filePath)) {
// Logger.e(TAG + "setLocalVideoMuteImage: filePath is empty")
callback?.onResponse("setLocalVideoMuteImage: filePath is empty")
return
}
val bitmap = BitmapFactory.decodeFile(filePath)
TUIRoomEngine.sharedInstance().setLocalVideoMuteImage(bitmap)
} catch (e: Exception) {
// Logger.e(TAG + "setLocalVideoMuteImage: ${e.message}")
callback?.onResponse("setLocalVideoMuteImage: unexpected error")
}
}
}
data class RequestData(
val api: String,
val params: Params?,
)
// 不要修改数据,每个数据对应一个关键字
data class Params(
val image: String?,
)

View File

@@ -0,0 +1,82 @@
package uts.sdk.modules.atomicx.kotlin
import android.content.Context
import android.graphics.Outline
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewOutlineProvider
import androidx.constraintlayout.widget.ConstraintLayout
import io.dcloud.uts.console
import io.trtc.tuikit.atomicxcore.api.view.CoreViewType
import io.trtc.tuikit.atomicxcore.api.view.LiveCoreView
private const val TAG = "UTS-LiveRenderView: "
class LiveRenderView(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
private var cornerRadius: Float = 48f // 圆角半径
private var nativeViewType = CoreViewType.PLAY_VIEW
init {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, cornerRadius)
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
outlineProvider.getOutline(this, Outline())
invalidateOutline()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.w(TAG, "onAttachedToWindow")
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.w(TAG, "onDetachedFromWindow")
}
public fun updateViewType(viewType: Any) {
if (viewType == null) {
return
}
if (viewType !is String) {
return
}
if (viewType == "PUSH_VIEW") {
nativeViewType = CoreViewType.PUSH_VIEW
}
}
public fun updateRenderView(liveID: Any) {
console.warn("StreamView, updateRenderView liveID: ", liveID, nativeViewType)
Logger.i(TAG + "updateRenderView: liveID:" + liveID + ", viewType: " + nativeViewType)
if (liveID == null) {
console.error("StreamView, updateRenderView liveID is invalid")
Logger.e(TAG + "updateRenderView: liveID is invalid")
return
}
if (liveID !is String) {
console.error("StreamView, updateRenderView liveID is not String")
Logger.e(TAG + "updateRenderView: liveID is not String")
return
}
if(liveID.isEmpty()) {
console.error("StreamView, updateRenderView liveID is empty")
Logger.e(TAG + "updateRenderView: liveID is empty")
return
}
removeAllViews()
val renderView = LiveCoreView(context, null, 0, nativeViewType)
renderView.setLiveId(liveID)
val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(renderView, lp)
}
}

View File

@@ -0,0 +1,79 @@
package uts.sdk.modules.atomicx.kotlin
import android.util.Log
import com.tencent.liteav.base.ContextUtils
import com.tencent.trtc.TRTCCloud
import org.json.JSONException
import org.json.JSONObject
class Logger {
companion object {
private const val API = "TuikitLog"
private const val LOG_KEY_API = "api"
private const val LOG_KEY_PARAMS = "params"
private const val LOG_KEY_PARAMS_LEVEL = "level"
private const val LOG_KEY_PARAMS_MESSAGE = "message"
private const val LOG_KEY_PARAMS_FILE = "file"
private const val LOG_KEY_PARAMS_LINE = "line"
private const val LOG_KEY_PARAMS_MODULE = "module"
private const val LOG_VALUE_PARAMS_MODULE = "call_state"
private const val LOG_LEVEL_INFO = 0
private const val LOG_LEVEL_WARNING = 1
private const val LOG_LEVEL_ERROR = 2
private const val LOG_FUNCTION_CALLER_INDEX = 5
fun i(message: String) {
log(LOG_LEVEL_INFO, message)
}
fun w(message: String) {
log(LOG_LEVEL_WARNING, message)
}
fun e(message: String) {
log(LOG_LEVEL_ERROR, message)
}
private fun log(
level: Int,
message: String,
) {
var context = ContextUtils.getApplicationContext()
if (context == null) {
ContextUtils.initContextFromNative("liteav")
context = ContextUtils.getApplicationContext()
}
if (context == null) {
return
}
try {
val paramsJson = JSONObject()
paramsJson.put(LOG_KEY_PARAMS_LEVEL, level)
paramsJson.put(LOG_KEY_PARAMS_MESSAGE, message)
paramsJson.put(LOG_KEY_PARAMS_MODULE, LOG_VALUE_PARAMS_MODULE)
paramsJson.put(LOG_KEY_PARAMS_FILE, getCallerFileName())
paramsJson.put(LOG_KEY_PARAMS_LINE, getCallerLineNumber())
val loggerJson = JSONObject()
loggerJson.put(LOG_KEY_API, API)
loggerJson.put(LOG_KEY_PARAMS, paramsJson)
TRTCCloud.sharedInstance(context).callExperimentalAPI(loggerJson.toString())
} catch (e: JSONException) {
Log.e("Logger", e.toString())
}
}
private fun getCallerFileName(): String {
val stackTrace = Thread.currentThread().stackTrace
if (stackTrace.size < LOG_FUNCTION_CALLER_INDEX + 1) return ""
return stackTrace[LOG_FUNCTION_CALLER_INDEX].fileName
}
private fun getCallerLineNumber(): Int {
val stackTrace = Thread.currentThread().stackTrace
if (stackTrace.size < LOG_FUNCTION_CALLER_INDEX + 1) return 0
return stackTrace[LOG_FUNCTION_CALLER_INDEX].lineNumber
}
}
}

View File

@@ -0,0 +1,141 @@
package uts.sdk.modules.atomicx.kotlin
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.opensource.svgaplayer.SVGACallback
import com.opensource.svgaplayer.SVGAImageView
import com.opensource.svgaplayer.SVGAParser
import com.opensource.svgaplayer.SVGAVideoEntity
import io.dcloud.uts.console
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.net.URL
class SVGAAnimationView(context: Context) : FrameLayout(context), SVGACallback {
private val svgaParser: SVGAParser
private val svgaImageView: SVGAImageView
private var svgaCallback: SVGACallback? = null
init {
svgaImageView = SVGAImageView(context)
val lp: FrameLayout.LayoutParams =
FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addView(svgaImageView, lp)
svgaImageView.loops = 1
svgaImageView.callback = this
svgaParser = SVGAParser.shareParser()
svgaParser.init(context)
}
fun setCallback(callback: SVGACallback) {
svgaCallback = callback
}
// val playUrl = "/sdcard/Android/data/uni.app.UNIFA697C8/images/sports_car.svga"
fun startAnimation(playUrl: String) {
console.log("startAnimation playUrl: ", playUrl)
// Logger.e(TAG + "startAnimation, playUrl: $playUrl")
if (playUrl.isNullOrEmpty()) {
console.error("startAnimation, playUrl is empty")
// Logger.e(TAG + "startAnimation, playUrl is empty")
svgaCallback?.onFinished()
return
}
if (playUrl.endsWith(".svga") && isUrl(playUrl)) {
decodeFromURL(playUrl)
} else {
decodeFromInputStream(playUrl)
}
}
private fun isUrl(url: String): Boolean = url.startsWith("http") || url.startsWith("https")
fun stopAnimation() {
svgaImageView.stopAnimation(true)
}
private fun decodeFromURL(playUrl: String) {
console.log("decodeFromURL, playUrl: ", playUrl)
svgaParser.decodeFromURL(URL(playUrl), object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
console.log("decodeFromURL onComplete, videoItem: ", videoItem)
// Logger.i(TAG + "startAnimation decodeFromURL, videoItem: $videoItem")
svgaImageView.setVisibility(View.VISIBLE)
svgaImageView.setVideoItem(videoItem)
svgaImageView.startAnimation()
}
override fun onError() {
console.log("===== decodeFromURL failed")
// Logger.e(TAG + "decodeFromURL failed, playUrl: $playUrl")
svgaCallback?.onFinished()
}
},)
}
private fun decodeFromInputStream(filePath: String) {
console.log("decodeFromInputStream, filePath: ", filePath)
val stream = openInputStream(filePath)
if (stream == null) {
console.log("decodeFromInputStream, filePath is null")
// Logger.e(TAG + "decodeFromInputStream failed, filePath is null")
return
}
svgaParser.decodeFromInputStream(stream, "", object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
console.log("======startAnimation decodeFromInputStream start: ", videoItem)
// Logger.i(TAG + "decodeFromInputStream start: videoItem: $videoItem")
svgaImageView.setVisibility(VISIBLE)
svgaImageView.setVideoItem(videoItem)
svgaImageView.startAnimation()
}
override fun onError() {
console.log("======decodeFromInputStream parse failed: ", filePath)
// Logger.e(TAG + "decodeFromInputStream parse failed, filePath: $filePath")
svgaCallback?.onFinished()
}
}, true, null, "", )
}
override fun onFinished() {
console.log("SVGAAnimationView onFinished")
// Logger.i(TAG + "onFinished")
svgaImageView.setVisibility(View.GONE)
svgaCallback?.onFinished()
}
override fun onPause() {
}
override fun onRepeat() {
}
override fun onStep(frame: Int, percentage: Double) {
}
private fun openInputStream(path: String): InputStream? {
try {
val file = File(path)
if (file.exists()) {
return FileInputStream(file)
}
} catch (e: FileNotFoundException) {
Log.i(TAG, " " + e.localizedMessage)
}
return null
}
companion object {
private const val TAG = "UTS-SVGAAnimationView: "
}
}

View File

@@ -0,0 +1,42 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.device.AudioEffectStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object AudioEffectStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun audioEffectStoreChanged(callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
AudioEffectStore.shared().audioEffectState.isEarMonitorOpened
.collect { enable ->
callback("isEarMonitorOpened", gson.toJson(enable))
}
}
launch {
AudioEffectStore.shared().audioEffectState.earMonitorVolume
.collect { volume ->
callback("earMonitorVolume", gson.toJson(volume))
}
}
launch {
AudioEffectStore.shared().audioEffectState.audioChangerType
.collect { type ->
callback("audioChangerType", gson.toJson(type.value))
}
}
launch {
AudioEffectStore.shared().audioEffectState.audioReverbType
.collect { type ->
callback("audioReverbType", gson.toJson(type.value))
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.barrage.Barrage
import io.trtc.tuikit.atomicxcore.api.barrage.BarrageStore
import io.trtc.tuikit.atomicxcore.api.barrage.BarrageType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object BarrageStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun barrageStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
BarrageStore.create(liveID).barrageState.messageList.collect { messageList ->
callback("messageList", gson.toJson(messageList))
}
}
// TODO: 底层未实现,暂时隐藏
// launch {
// BarrageStore.create(liveID).barrageState.allowSendMessage.collect { allowSendMessage ->
// callback("allowSendMessage", gson.toJson(allowSendMessage))
// }
// }
}
}
}

View File

@@ -0,0 +1,34 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.device.BaseBeautyStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object BaseBeautyStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun beautyStoreChanged(callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
BaseBeautyStore.shared().baseBeautyState.smoothLevel.collect { level ->
callback("smoothLevel", gson.toJson(level))
}
}
launch {
BaseBeautyStore.shared().baseBeautyState.whitenessLevel.collect { level ->
callback("whitenessLevel", gson.toJson(level))
}
}
launch {
BaseBeautyStore.shared().baseBeautyState.ruddyLevel.collect { level ->
callback("ruddyLevel", gson.toJson(level))
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.BattleStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object BattleStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun battleStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
BattleStore.create(liveID).battleState.currentBattleInfo.collect { currentBattleInfo ->
callback("currentBattleInfo", gson.toJson(currentBattleInfo))
}
}
launch {
BattleStore.create(liveID).battleState.battleUsers.collect { battleUsers ->
callback("battleUsers", gson.toJson(battleUsers))
}
}
launch {
BattleStore.create(liveID).battleState.battleScore.collect { battleScore ->
callback("battleScore", gson.toJson(battleScore))
}
}
}
}
}

View File

@@ -0,0 +1,72 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.CoGuestStore
import io.trtc.tuikit.atomicxcore.api.device.DeviceStatus
import io.trtc.tuikit.atomicxcore.api.live.Role
import io.trtc.tuikit.atomicxcore.api.live.SeatUserInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object CoGuestStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun coGuestStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
CoGuestStore.create(liveID).coGuestState.connected.collect { connected ->
val list = connected.map { convertSeatInfoToMap(it) }
callback("connected", gson.toJson(list)) // SeatUserInfo
}
}
launch {
CoGuestStore.create(liveID).coGuestState.invitees.collect { invitees ->
callback("invitees", gson.toJson(invitees)) // LiveUserInfo
}
}
launch {
CoGuestStore.create(liveID).coGuestState.applicants.collect { applicants ->
callback("applicants", gson.toJson(applicants)) // LiveUserInfo
}
}
launch {
CoGuestStore.create(liveID).coGuestState.candidates.collect { candidates ->
callback("candidates", gson.toJson(candidates)) // LiveUserInfo
}
}
}
}
private fun convertSeatInfoToMap(info: SeatUserInfo): Map<String, Any> {
val map = mutableMapOf<String, Any>()
map["userID"] = info.userID
map["userName"] = info.userName
map["avatarURL"] = info.avatarURL
map["role"] = info.role
map["liveID"] = info.liveID
map["microphoneStatus"] = convertDeviceStatus(info.microphoneStatus)
map["allowOpenMicrophone"] = info.allowOpenMicrophone
map["cameraStatus"] = convertDeviceStatus(info.cameraStatus)
map["allowOpenCamera"] = info.allowOpenCamera
return map
}
private fun convertDeviceStatus(status: DeviceStatus?): String {
if (status == DeviceStatus.ON) {
return "ON"
}
return "OFF"
}
private fun convertUserRole(role: Role?): String {
return when (role) {
Role.OWNER -> "OWNER"
Role.ADMIN -> "ADMIN"
else -> "GENERAL_USER"
}
}
}

View File

@@ -0,0 +1,45 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.CoHostStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object CoHostStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun coHostStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
CoHostStore.create(liveID).coHostState.coHostStatus.collect { coHostStatus ->
callback("coHostStatus", gson.toJson(coHostStatus))
}
}
launch {
CoHostStore.create(liveID).coHostState.connected.collect { connected ->
callback("connected", gson.toJson(connected))
}
}
// TODO: 底层未实现,暂时隐藏
// launch {
// CoHostStore.create(liveID).coHostState.candidates.collect { candidates ->
// callback("candidates", gson.toJson(candidates))
// }
// }
launch {
CoHostStore.create(liveID).coHostState.invitees.collect { invitees ->
callback("invitees", gson.toJson(invitees))
}
}
launch {
CoHostStore.create(liveID).coHostState.applicant.collect { applicant ->
callback("applicant", gson.toJson(applicant))
}
}
}
}
}

View File

@@ -0,0 +1,87 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object DeviceStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun deviceStoreChanged(callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
DeviceStore.shared().deviceState.microphoneStatus.collect { status ->
callback("microphoneStatus", gson.toJson(status.value))
}
}
launch {
DeviceStore.shared().deviceState.microphoneLastError.collect { deviceError ->
callback("microphoneLastError", gson.toJson(deviceError.value))
}
}
launch {
DeviceStore.shared().deviceState.captureVolume.collect { volume ->
callback("captureVolume", gson.toJson(volume))
}
}
launch {
DeviceStore.shared().deviceState.currentMicVolume.collect { volume ->
callback("currentMicVolume", gson.toJson(volume))
}
}
launch {
DeviceStore.shared().deviceState.outputVolume.collect { volume ->
callback("outputVolume", gson.toJson(volume))
}
}
launch {
DeviceStore.shared().deviceState.cameraStatus.collect { cameraStatus ->
callback("cameraStatus", gson.toJson(cameraStatus.value))
}
}
launch {
DeviceStore.shared().deviceState.cameraLastError.collect { deviceError ->
callback("cameraLastError", gson.toJson(deviceError.value))
}
}
launch {
DeviceStore.shared().deviceState.isFrontCamera.collect { isFrontCamera ->
callback("isFrontCamera", gson.toJson(isFrontCamera))
}
}
launch {
DeviceStore.shared().deviceState.localMirrorType.collect { localMirrorType ->
callback("localMirrorType", gson.toJson(localMirrorType))
}
}
launch {
DeviceStore.shared().deviceState.localVideoQuality.collect { quality ->
callback("localVideoQuality", gson.toJson(quality))
}
}
launch {
DeviceStore.shared().deviceState.currentAudioRoute.collect { audioRoute ->
callback("currentAudioRoute", gson.toJson(audioRoute.value))
}
}
launch {
DeviceStore.shared().deviceState.screenStatus.collect { screenStatus ->
callback("screenStatus", gson.toJson(screenStatus.value))
}
}
launch {
DeviceStore.shared().deviceState.networkInfo.collect { networkInfo ->
callback("networkInfo", gson.toJson(networkInfo))
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.gift.GiftStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object GiftStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun giftStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
GiftStore.create(liveID).giftState.usableGifts.collect { usableGifts ->
callback("usableGifts", gson.toJson(usableGifts))
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.LikeStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object LikeStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun likeStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LikeStore.create(liveID).likeState.totalLikeCount.collect { count ->
callback("totalLikeCount", gson.toJson(count))
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.LiveAudienceStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object LiveAudienceStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun liveAudienceStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LiveAudienceStore.create(liveID).liveAudienceState.audienceList.collect { audienceList ->
callback("audienceList", gson.toJson(audienceList))
}
}
launch {
LiveAudienceStore.create(liveID).liveAudienceState.audienceCount.collect { audienceCount ->
callback("audienceCount", gson.toJson(audienceCount))
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.LiveListStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object LiveListStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun liveStoreChanged(callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LiveListStore.shared().liveState.liveList.collect { liveList ->
callback("liveList", gson.toJson(liveList))
}
}
launch {
LiveListStore.shared().liveState.liveListCursor.collect { cursor ->
callback("liveListCursor", gson.toJson(cursor))
}
}
launch {
LiveListStore.shared().liveState.currentLive.collect { liveInfo ->
callback("currentLive", gson.toJson(liveInfo))
}
}
}
}
}

View File

@@ -0,0 +1,39 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.LiveSeatStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import uts.sdk.modules.atomicx.kotlin.Logger
import io.dcloud.uts.console
object LiveSeatStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun liveSeatStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LiveSeatStore.create(liveID).liveSeatState.seatList.collect { seatList ->
val list = gson.toJson(seatList)
console.info("UTS-Live: liveSeatStoreChanged, seatList: ", list)
Logger.i("UTS-Live: " + "liveSeatStoreChanged, seatList: "+ list);
callback("seatList", gson.toJson(seatList))
}
}
launch {
LiveSeatStore.create(liveID).liveSeatState.canvas.collect { canvas ->
callback("canvas", gson.toJson(canvas))
}
}
launch {
LiveSeatStore.create(liveID).liveSeatState.speakingUsers.collect { speakingUsers ->
callback("speakingUsers", gson.toJson(speakingUsers))
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import io.trtc.tuikit.atomicxcore.api.live.LiveSummaryStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object LiveSummaryStoreObserver {
private val gson = Gson()
private var bindDataJob: Job? = null
fun liveSummaryStoreChanged(liveID: String, callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LiveSummaryStore.create(liveID).liveSummaryState.summaryData.collect { data ->
callback("summaryData", gson.toJson(data))
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
package uts.sdk.modules.atomicx.observer
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import io.trtc.tuikit.atomicxcore.api.login.LoginStatus
import io.trtc.tuikit.atomicxcore.api.login.LoginStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object LoginStoreObserver {
private val gson = GsonBuilder().serializeNulls().create()
private var bindDataJob: Job? = null
fun loginStoreChanged(callback: (String, String) -> Unit) {
bindDataJob?.cancel()
bindDataJob = CoroutineScope(Dispatchers.Main).launch {
launch {
LoginStore.shared.loginState.loginUserInfo.collect { userInfo ->
callback("loginUserInfo", gson.toJson(userInfo))
}
}
launch {
LoginStore.shared.loginState.loginStatus.collect { loginStatus ->
// UNLOGIN \ LOGINED
callback("loginStatus", gson.toJson(loginStatus))
}
}
}
}
}

View File

@@ -0,0 +1,693 @@
import { ILiveListener } from '../../interface';
import { GiftListener, Gift, } from 'io.trtc.tuikit.atomicxcore.api.gift';
import {
LiveUserInfo, LikeListener, LiveAudienceListener, LiveListListener,
LiveEndedReason, LiveKickedOutReason, LiveSeatListener, DeviceControlPolicy,
HostListener, GuestListener, NoResponseReason, CoHostListener, SeatUserInfo,
BattleListener, BattleInfo, BattleEndedReason,
} from 'io.trtc.tuikit.atomicxcore.api.live';
const TAG = "UTS-Event"
export const giftListenerMap = new Map<string, Array<ILiveListener>>();
export const likeListenerMap = new Map<string, Array<ILiveListener>>();
export const audienceListenerMap = new Map<string, Array<ILiveListener>>();
export const liveListListenerMap = new Map<string, Array<ILiveListener>>();
export const liveSeatListenerMap = new Map<string, Array<ILiveListener>>();
export const coGuestHostListenerMap = new Map<string, Array<ILiveListener>>();
export const coGuestGuestListenerMap = new Map<string, Array<ILiveListener>>();
export const coHostListenerMap = new Map<string, Array<ILiveListener>>();
export const battleListenerMap = new Map<string, Array<ILiveListener>>();
export type NativeLiveListener = {
listener : (eventName : string, data : string) => void
}
// 创建LiveList事件分发器
export function createLiveListEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
liveListListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建 LiveAudience 事件分发器
export function createAudienceEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
audienceListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建CoHost事件分发器
export function createCoHostEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
coHostListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建CoGuestHost事件分发器
export function createCoGuestHostEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
coGuestHostListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建CoGuestGuest事件分发器
export function createCoGuestGuestEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
coGuestGuestListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建Gift事件分发器
export function createGiftEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
giftListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建Like事件分发器
export function createLikeEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
likeListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建Battle事件分发器
export function createBattleEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
battleListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
// 创建LiveSeat事件分发器
export function createLiveSeatEventDispatcher() {
const liveListener : NativeLiveListener = {
listener: (eventName : string, data : string) => {
liveSeatListenerMap.get(eventName)?.forEach((iLiveListener) => {
iLiveListener.callback(data);
});
},
};
return liveListener
}
export class TGiftListener extends GiftListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onReceiveGift(liveID : String, gift : Gift, count : Int, sender : LiveUserInfo) {
console.log(`${TAG} onReceiveGift liveID: ${liveID},gift:${gift} ,count: ${count}, sender: ${sender}`);
const giftData = {
giftID: gift.giftID,
name: gift.name,
desc: gift.desc,
iconURL: gift.iconURL,
resourceURL: gift.resourceURL,
level: gift.level,
coins: gift.coins,
extensionInfo: gift.extensionInfo
};
const senderUser = {
userID: sender.userID,
userName: sender.userName,
avatarURL: sender.avatarURL
}
let data = {
liveID,
gift: giftData,
count,
sender: senderUser
}
this.listener('onReceiveGift', JSON.stringify(data));
}
}
export class TLikeListener extends LikeListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onReceiveLikesMessage(liveID : String, totalLikesReceived : Long, sender : LiveUserInfo) {
console.log(`${TAG} onReceiveLikesMessage liveID: ${liveID}, totalLikesReceived: ${totalLikesReceived}, sender: ${sender}`);
const senderUser = {
userID: sender.userID,
userName: sender.userName,
avatarURL: sender.avatarURL
};
let data = {
liveID,
totalLikesReceived,
sender: senderUser
};
this.listener('onReceiveLikesMessage', JSON.stringify(data));
}
}
export class TLiveAudienceListener extends LiveAudienceListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onAudienceJoined(audience : LiveUserInfo) {
console.log(`${TAG} onAudienceJoined audience: ${audience}`);
const audienceUser = {
userID: audience.userID,
userName: audience.userName,
avatarURL: audience.avatarURL
};
let data = {
audience: audienceUser
}
this.listener('onAudienceJoined', JSON.stringify(data));
}
override onAudienceLeft(audience : LiveUserInfo) {
console.log(`${TAG} onAudienceLeft audience: ${audience}`);
const audienceUser = {
userID: audience.userID,
userName: audience.userName,
avatarURL: audience.avatarURL
};
let data = {
audience: audienceUser
}
this.listener('onAudienceLeft', JSON.stringify(data));
}
override onAudienceMessageDisabled(audience : LiveUserInfo, isDisable : Boolean) {
console.log(`${TAG} onAudienceMessageDisabled audience: ${audience}`);
const audienceUser = {
userID: audience.userID,
userName: audience.userName,
avatarURL: audience.avatarURL
};
let data = {
audience: audienceUser,
isDisable
}
this.listener('onAudienceMessageDisabled', JSON.stringify(data));
}
}
export class TLiveListListener extends LiveListListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onLiveEnded(liveID : String, reason : LiveEndedReason, message : String) {
let reasonStr = "ENDED_BY_HOST"
if (reason == LiveEndedReason.ENDED_BY_SERVER) {
reasonStr = "ENDED_BY_SERVER"
}
let data = {
liveID,
reason: reasonStr,
message
};
console.log(`${TAG} onLiveEnded, data : ${JSON.stringify(data)}`)
this.listener('onLiveEnded', JSON.stringify(data));
}
override onKickedOutOfLive(liveID : String, reason : LiveKickedOutReason, message : String) {
let data = {
liveID,
reason: this.convertKickedOutReasonToString(reason),
message
};
console.log(`${TAG} onKickedOutOfLive, data : ${JSON.stringify(data)}`);
this.listener('onKickedOutOfLive', JSON.stringify(data));
}
private convertKickedOutReasonToString(reason : LiveKickedOutReason) : String {
switch (reason) {
case LiveKickedOutReason.BY_LOGGED_ON_OTHER_DEVICE:
return "BY_LOGGED_ON_OTHER_DEVICE"
case LiveKickedOutReason.BY_SERVER:
return "BY_SERVER"
case LiveKickedOutReason.FOR_NETWORK_DISCONNECTED:
return "FOR_NETWORK_DISCONNECTED"
case LiveKickedOutReason.FOR_JOIN_ROOM_STATUS_INVALID_DURING_OFFLINE:
return "FOR_JOIN_ROOM_STATUS_INVALID_DURING_OFFLINE"
case LiveKickedOutReason.FOR_COUNT_OF_JOINED_ROOMS_EXCEED_LIMIT:
return "FOR_COUNT_OF_JOINED_ROOMS_EXCEED_LIMIT"
default:
return "BY_ADMIN"
}
}
}
export class TLiveSeatListener extends LiveSeatListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onLocalCameraOpenedByAdmin(policy : DeviceControlPolicy) {
console.log(`${TAG} onLocalCameraOpenedByAdmin policy: ${policy}`);
let devicePolicy = "FORCE_OPEN"
if (policy == DeviceControlPolicy.UNLOCK_ONLY) {
devicePolicy = "UNLOCK_ONLY"
}
this.listener('onLocalCameraOpenedByAdmin', devicePolicy);
}
override onLocalCameraClosedByAdmin() {
console.log(`${TAG} onLocalCameraClosedByAdmin`);
this.listener('onLocalCameraClosedByAdmin', "");
}
override onLocalMicrophoneOpenedByAdmin(policy : DeviceControlPolicy) {
console.log(`${TAG} onLocalMicrophoneOpenedByAdmin policy: ${policy}`);
let devicePolicy = "FORCE_OPEN"
if (policy == DeviceControlPolicy.UNLOCK_ONLY) {
devicePolicy = "UNLOCK_ONLY"
}
this.listener('onLocalMicrophoneOpenedByAdmin', devicePolicy);
}
override onLocalMicrophoneClosedByAdmin() {
console.log(`${TAG} onLocalMicrophoneClosedByAdmin`);
this.listener('onLocalMicrophoneClosedByAdmin', "");
}
}
// CoGuestStore: HostListener
export class TCoGuestHostListener extends HostListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onGuestApplicationReceived(guestUser : LiveUserInfo) {
console.log(`${TAG} onGuestApplicationReceived guestUser: ${guestUser}`);
const guestUserInfo = {
userID: guestUser.userID,
userName: guestUser.userName,
avatarURL: guestUser.avatarURL
};
let data = {
guestUser: guestUserInfo,
};
this.listener('onGuestApplicationReceived', JSON.stringify(data));
}
override onGuestApplicationCancelled(guestUser : LiveUserInfo) {
console.log(`${TAG} onGuestApplicationCancelled guestUser: ${guestUser}`);
const guestUserInfo = {
userID: guestUser.userID,
userName: guestUser.userName,
avatarURL: guestUser.avatarURL
};
let data = {
guestUser: guestUserInfo,
};
this.listener('onGuestApplicationCancelled', JSON.stringify(data));
}
override onGuestApplicationProcessedByOtherHost(guestUser : LiveUserInfo, hostUser : LiveUserInfo) {
console.log(`${TAG} onGuestApplicationProcessedByOtherHost guestUser: ${guestUser}, hostUser: ${hostUser}`);
const guestUserInfo = {
userID: guestUser.userID,
userName: guestUser.userName,
avatarURL: guestUser.avatarURL
};
const hostUserInfo = {
userID: hostUser.userID,
userName: hostUser.userName,
avatarURL: hostUser.avatarURL
};
let data = {
guestUser: guestUserInfo,
hostUser: hostUserInfo
};
this.listener('onGuestApplicationProcessedByOtherHost', JSON.stringify(data));
}
override onHostInvitationResponded(isAccept : Boolean, guestUser : LiveUserInfo) {
console.log(`${TAG} onHostInvitationResponded isAccept: ${isAccept}, guestUser: ${guestUser}`);
const guestUserInfo = {
userID: guestUser.userID,
userName: guestUser.userName,
avatarURL: guestUser.avatarURL
};
let data = {
isAccept,
guestUser: guestUserInfo
};
this.listener('onHostInvitationResponded', JSON.stringify(data));
}
override onHostInvitationNoResponse(guestUser : LiveUserInfo, reason : NoResponseReason) {
console.log(`${TAG} onHostInvitationNoResponse guestUser: ${guestUser}, reason: ${reason}`);
const guestUserInfo = {
userID: guestUser.userID,
userName: guestUser.userName,
avatarURL: guestUser.avatarURL
};
let data = {
guestUser: guestUserInfo,
reason: reason
};
this.listener('onHostInvitationNoResponse', JSON.stringify(data));
}
}
// CoGuestStore: GuestListener
export class TCoGuestGuestListener extends GuestListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
override onHostInvitationReceived(hostUser : LiveUserInfo) {
const hostUserInfo = {
userID: hostUser.userID,
userName: hostUser.userName,
avatarURL: hostUser.avatarURL
};
let data = {
hostUser: hostUserInfo,
};
console.log(`${TAG} onHostInvitationReceived hostUser: ${JSON.stringify(data)}`);
this.listener('onHostInvitationReceived', JSON.stringify(data));
}
override onHostInvitationCancelled(hostUser : LiveUserInfo) {
const hostUserInfo = {
userID: hostUser.userID,
userName: hostUser.userName,
avatarURL: hostUser.avatarURL
};
let data = {
hostUser: hostUserInfo,
};
console.log(`${TAG} onHostInvitationCancelled hostUser: ${JSON.stringify(data)}`);
this.listener('onHostInvitationCancelled', JSON.stringify(data));
}
override onGuestApplicationResponded(isAccept : Boolean, hostUser : LiveUserInfo) {
const hostUserInfo = {
userID: hostUser.userID,
userName: hostUser.userName,
avatarURL: hostUser.avatarURL
};
let data = {
isAccept,
hostUser: hostUserInfo
};
console.log(`${TAG} onGuestApplicationResponded isAccept: ${isAccept}, hostUser: ${JSON.stringify(data)}`);
this.listener('onGuestApplicationResponded', JSON.stringify(data));
}
override onGuestApplicationNoResponse(reason : NoResponseReason) {
let reasonStr = "TIMEOUT"
if (reason == NoResponseReason.ALREADY_SEATED) {
reasonStr = "ALREADY_SEATED"
}
let data = {
reason: reasonStr,
}
console.log(`${TAG} onGuestApplicationNoResponse reason: ${JSON.stringify(data)}`);
this.listener('onGuestApplicationNoResponse', JSON.stringify(data));
}
override onKickedOffSeat(seatIndex : Int, hostUser : LiveUserInfo) {
console.log(`${TAG} onKickedOffSeat seatIndex: ${seatIndex}, hostUser: ${hostUser}`);
const hostUserInfo = {
userID: hostUser.userID,
userName: hostUser.userName,
avatarURL: hostUser.avatarURL
};
let data = {
seatIndex,
hostUser: hostUserInfo
};
this.listener('onKickedOffSeat', JSON.stringify(data));
}
}
//CoHostStore: CoHostListener
export class TCoHostListener extends CoHostListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
private convertSeatUserInfoToData(seatUserInfo ?: SeatUserInfo) {
return {
userID: seatUserInfo?.userID,
userName: seatUserInfo?.userName,
avatarURL: seatUserInfo?.avatarURL,
role: seatUserInfo?.role,
liveID: seatUserInfo?.liveID,
microphoneStatus: seatUserInfo?.microphoneStatus,
allowOpenMicrophone: seatUserInfo?.allowOpenMicrophone,
cameraStatus: seatUserInfo?.cameraStatus,
allowOpenCamera: seatUserInfo?.allowOpenCamera
};
}
override onCoHostRequestReceived(inviter : SeatUserInfo, extensionInfo : String) {
console.log(`${TAG} onCoHostRequestReceived inviter: ${inviter}, extensionInfo: ${extensionInfo}`);
let data = {
inviter: this.convertSeatUserInfoToData(inviter),
extensionInfo: extensionInfo ?? ""
};
this.listener('onCoHostRequestReceived', JSON.stringify(data));
}
override onCoHostRequestCancelled(inviter : SeatUserInfo, invitee ?: SeatUserInfo) {
console.log(`${TAG} onCoHostRequestCancelled inviter: ${inviter}, invitee: ${invitee}`);
let data = {
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
this.listener('onCoHostRequestCancelled', JSON.stringify(data));
}
override onCoHostRequestAccepted(invitee : SeatUserInfo) {
console.log(`${TAG} onCoHostRequestAccepted invitee: ${invitee}`);
let data = {
invitee: this.convertSeatUserInfoToData(invitee)
};
this.listener('onCoHostRequestAccepted', JSON.stringify(data));
}
override onCoHostRequestRejected(invitee : SeatUserInfo) {
console.log(`${TAG} onCoHostRequestRejected invitee: ${invitee}`);
let data = {
invitee: this.convertSeatUserInfoToData(invitee)
};
this.listener('onCoHostRequestRejected', JSON.stringify(data));
}
override onCoHostRequestTimeout(inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onCoHostRequestTimeout inviter: ${inviter}`);
let data = {
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee),
};
this.listener('onCoHostRequestTimeout', JSON.stringify(data));
}
override onCoHostUserJoined(userInfo : SeatUserInfo) {
console.log(`${TAG} onCoHostUserJoined userInfo: ${userInfo}`);
let data = {
userInfo: this.convertSeatUserInfoToData(userInfo)
}
this.listener('onCoHostUserJoined', JSON.stringify(data));
}
override onCoHostUserLeft(userInfo : SeatUserInfo) {
console.log(`${TAG} onCoHostUserLeft userInfo: ${userInfo}`);
let data = {
userInfo: this.convertSeatUserInfoToData(userInfo)
}
this.listener('onCoHostUserLeft', JSON.stringify(data));
}
}
// BattleStore: BattleListener
export class TBattleListener extends BattleListener {
private listener : (eventType : string, data : string) => void;
constructor(options : NativeLiveListener) {
super();
this.listener = options.listener;
}
private convertSeatUserInfoToData(seatUserInfo ?: SeatUserInfo) {
return {
userID: seatUserInfo?.userID,
userName: seatUserInfo?.userName,
avatarURL: seatUserInfo?.avatarURL,
role: seatUserInfo?.role,
liveID: seatUserInfo?.liveID,
microphoneStatus: seatUserInfo?.microphoneStatus,
allowOpenMicrophone: seatUserInfo?.allowOpenMicrophone,
cameraStatus: seatUserInfo?.cameraStatus,
allowOpenCamera: seatUserInfo?.allowOpenCamera
};
}
private convertBattleInfoToData(battleInfo : BattleInfo) {
let battleConfig = {
duration: battleInfo.config.duration,
needResponse: battleInfo.config.needResponse,
extensionInfo: battleInfo.config.extensionInfo
}
return {
battleID: battleInfo.battleID,
config: battleConfig,
startTime: battleInfo.startTime,
endTime: battleInfo.endTime
};
}
override onBattleStarted(battleInfo : BattleInfo, inviter : SeatUserInfo, invitees : List<SeatUserInfo>) {
console.log(`${TAG} onBattleStarted battleInfo: ${battleInfo}, inviter: ${inviter}, invitees: ${invitees}`);
let data = {
battleInfo: this.convertBattleInfoToData(battleInfo),
inviter: this.convertSeatUserInfoToData(inviter),
invitees: invitees.map(user => this.convertSeatUserInfoToData(user))
};
console.log(`${TAG} onBattleStarted data: ${JSON.stringify(data)}`);
this.listener('onBattleStarted', JSON.stringify(data));
}
override onBattleEnded(battleInfo : BattleInfo, reason ?: BattleEndedReason) {
console.log(`${TAG} onUserJoinBattle battleInfo: ${battleInfo}, reason: ${reason}`);
let reasonStr = "TIME_OVER"
if (reason == BattleEndedReason.ALL_MEMBER_EXIT) {
reasonStr = "ALL_MEMBER_EXIT"
}
let data = {
battleInfo: this.convertBattleInfoToData(battleInfo),
reason: reasonStr,
};
console.log(`${TAG} onBattleEnded data: ${JSON.stringify(data)}`);
this.listener('onBattleEnded', JSON.stringify(data));
}
override onUserJoinBattle(battleID : String, battleUser : SeatUserInfo) {
console.log(`${TAG} onUserJoinBattle battleID: ${battleID}, battleUser: ${battleUser}`);
let data = {
battleID,
battleUser: this.convertSeatUserInfoToData(battleUser)
};
console.log(`${TAG} onUserJoinBattle data: ${JSON.stringify(data)}`);
this.listener('onUserJoinBattle', JSON.stringify(data));
}
override onUserExitBattle(battleID : String, battleUser : SeatUserInfo) {
console.log(`${TAG} onUserExitBattle battleID: ${battleID}, battleUser: ${battleUser}`);
let data = {
battleID,
battleUser: this.convertSeatUserInfoToData(battleUser)
};
console.log(`${TAG} onUserExitBattle data: ${JSON.stringify(data)}`);
this.listener('onUserExitBattle', JSON.stringify(data));
}
override onBattleRequestReceived(battleID : String, inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onBattleRequestReceived battleID: ${battleID}, inviter: ${inviter}, invitee: ${invitee}`);
let data = {
battleID,
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
console.log(`${TAG} onBattleRequestReceived data: ${JSON.stringify(data)}`);
this.listener('onBattleRequestReceived', JSON.stringify(data));
}
override onBattleRequestCancelled(battleID : String, inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onBattleRequestCancelled battleID: ${battleID}, inviter: ${inviter}, invitee: ${invitee}`);
let data = {
battleID,
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
console.log(`${TAG} onBattleRequestCancelled data: ${JSON.stringify(data)}`);
this.listener('onBattleRequestCancelled', JSON.stringify(data));
}
override onBattleRequestTimeout(battleID : String, inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onBattleRequestTimeout battleID: ${battleID}, inviter: ${inviter}, invitee: ${invitee}`);
let data = {
battleID,
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
console.log(`${TAG} onBattleRequestTimeout data: ${JSON.stringify(data)}`);
this.listener('onBattleRequestTimeout', JSON.stringify(data));
}
override onBattleRequestAccept(battleID : String, inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onBattleRequestAccept battleID: ${battleID}, inviter: ${inviter}, invitee: ${invitee}`);
let data = {
battleID,
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
console.log(`${TAG} onBattleRequestAccept data: ${JSON.stringify(data)}`);
this.listener('onBattleRequestAccept', JSON.stringify(data));
}
override onBattleRequestReject(battleID : String, inviter : SeatUserInfo, invitee : SeatUserInfo) {
console.log(`${TAG} onBattleRequestReject battleID: ${battleID}, inviter: ${inviter}, invitee: ${invitee}`);
let data = {
battleID,
inviter: this.convertSeatUserInfoToData(inviter),
invitee: this.convertSeatUserInfoToData(invitee)
};
console.log(`${TAG} onBattleRequestReject data: ${JSON.stringify(data)}`);
this.listener('onBattleRequestReject', JSON.stringify(data));
}
}

View File

@@ -0,0 +1,76 @@
<template>
<view class="defaultStyles"> </view>
</template>
<script lang="uts">
import LiveRenderView from 'uts.sdk.modules.atomicx.kotlin.LiveRenderView';
import Log from "android.util.Log"
const STREAM_TAG = "LiveRenderView"
let liveID : Any = ""
export default {
name: "live-core-view",
props: {
"liveID": {
type: Any,
default: ""
},
"viewType": {
type: String,
default: "PLAY_VIEW"
},
},
watch: {
"liveID": {
handler(newValue : Any, oldValue : Any) {
console.log(`${STREAM_TAG} liveID newValue, ${newValue}`);
Log.e(STREAM_TAG, "liveID newValue, " + newValue)
liveID = newValue
this.$el?.updateRenderView(newValue)
},
immediate: true // 创建时是否通过此方法更新属性默认值为false
},
"viewType": {
handler(newValue : String, oldValue : String) {
console.log(`${STREAM_TAG} liveID newValue, ${newValue}`);
this.$el?.updateViewType(newValue)
},
immediate: true
},
},
created() {
console.log(`${STREAM_TAG} created`);
Log.e(STREAM_TAG, "created ")
},
NVLoad() : LiveRenderView {
let streamView = new LiveRenderView($androidContext!)
streamView.updateRenderView(liveID)
console.log(`${STREAM_TAG} NVLoad, ${streamView}`);
Log.e(STREAM_TAG, "NVLoad ")
return streamView;
},
NVLoaded() {
console.log(`${STREAM_TAG} NVLoaded`);
Log.e(STREAM_TAG, "NVLoaded ")
},
NVLayouted() {
},
NVBeforeUnload() {
},
NVUnloaded() {
console.log(`${STREAM_TAG} NVUnloaded`);
Log.e(STREAM_TAG, "NVUnloaded ")
},
unmounted() {
console.log(`${STREAM_TAG} unmounted`);
Log.e(STREAM_TAG, "unmounted ")
}
}
</script>
<style>
/* 定义默认样式值, 组件使用者没有配置时使用 */
.defaultStyles {
background-color: white;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<view class="defaultStyles"> </view>
</template>
<script lang="uts">
import Log from "android.util.Log"
import SVGAAnimationView from "uts.sdk.modules.atomicx.kotlin.SVGAAnimationView";
import SVGACallback from "com.opensource.svgaplayer.SVGACallback"
const SVGA_TAG = "SvgaPlayer"
export default {
name: "svga-player",
/**
* 组件涉及的事件声明,只有声明过的事件,才能被正常发送
*/
emits: ['onFinished'],
/**
* 规则如果没有配置expose则 methods 中的方法均对外暴露如果配置了expose则以expose的配置为准向外暴露
* ['publicMethod'] 含义为:只有 `publicMethod` 在实例上可用
*/
expose: ['startPlay', 'stopPlay'],
methods: {
startPlay(url : string) {
console.log(`${SVGA_TAG} startAnimation, url: ${url}`);
Log.e(SVGA_TAG, "startAnimation, url: " + url)
this.$el?.startAnimation(url)
},
stopPlay() {
console.log(`${SVGA_TAG} stop`);
Log.e(SVGA_TAG, "stopAnimation ")
this.$el?.stopAnimation()
}
},
created() {
console.log(`${SVGA_TAG} created`);
Log.e(SVGA_TAG, "created ")
},
NVLoad() : SVGAAnimationView {
let svgaView = new SVGAAnimationView($androidContext!)
svgaView.setCallback(new InnerSVGACallback(this))
console.log(`${SVGA_TAG} NVLoad, ${svgaView}`);
Log.e(SVGA_TAG, "NVLoad ")
return svgaView;
},
NVLoaded() {
},
NVLayouted() {
},
NVUnloaded() {
console.log(`${SVGA_TAG} NVUnloaded`);
Log.e(SVGA_TAG, "NVUnloaded ")
},
unmounted() {
console.log(`${SVGA_TAG} unmounted`);
Log.e(SVGA_TAG, "unmounted ")
}
}
class InnerSVGACallback extends SVGACallback {
private comp : UTSComponent<SVGAAnimationView>;
constructor(comp : UTSComponent<SVGAAnimationView>) {
super();
this.comp = comp;
}
override onPause() { }
override onFinished() {
console.log("SvgaPlayer onFinished")
this.comp.$emit("onFinished")
}
override onRepeat() { }
override onStep(frame : Int, percentage : Double) { }
}
</script>
<style>
/* 定义默认样式值, 组件使用者没有配置时使用 */
.defaultStyles {
background-color: white;
}
</style>

View File

@@ -0,0 +1,216 @@
import {
UserProfileParam, LiveInfoParam, TakeSeatModeType, LiveUserInfoParam, MoveSeatPolicyType,
VideoQualityType, BarrageParam, MessageType, AudioReverbTypeParam, AudioChangerTypeParam,
LiveModifyFlag, AppendLocalTipOptions,
} from '../../interface.uts';
import { UserProfile, AllowType, Gender, } from 'io.trtc.tuikit.atomicxcore.api.login';
import { LiveInfo, LiveUserInfo, TakeSeatMode, MoveSeatPolicy, } from 'io.trtc.tuikit.atomicxcore.api.live';
import { VideoQuality, AudioChangerType, AudioReverbType, } from 'io.trtc.tuikit.atomicxcore.api.device';
import { Barrage, BarrageType, } from 'io.trtc.tuikit.atomicxcore.api.barrage';
export class ParamsCovert {
public static convertUserProfile(userProfile : UserProfileParam) : UserProfile {
const nativeUserProfile = new UserProfile()
nativeUserProfile.userID = userProfile.userID ?? ""
nativeUserProfile.nickname = userProfile.nickname
nativeUserProfile.avatarURL = userProfile.avatarURL
nativeUserProfile.selfSignature = userProfile.selfSignature
nativeUserProfile.gender = Gender.parse(userProfile.gender?.toInt() ?? 0)
nativeUserProfile.role = userProfile.role?.toInt()
nativeUserProfile.level = userProfile.level?.toInt()
nativeUserProfile.birthday = userProfile.birthday?.toLong()
nativeUserProfile.allowType = AllowType.parse(userProfile.allowType?.toInt() ?? 0)
//nativeUserProfile.customInfo = userProfile.customInfo //TODO: ByteArray转换
return nativeUserProfile
}
public static convertLiveInfo(liveInfo : LiveInfoParam) : LiveInfo {
const nativeLiveInfo = new LiveInfo()
nativeLiveInfo.liveID = liveInfo.liveID
nativeLiveInfo.liveName = liveInfo.liveName ?? ""
nativeLiveInfo.notice = liveInfo.notice ?? ""
nativeLiveInfo.isMessageDisable = liveInfo.isMessageDisable ?? false
nativeLiveInfo.isPublicVisible = liveInfo.isPublicVisible ?? true
nativeLiveInfo.isSeatEnabled = liveInfo.isSeatEnabled ?? true
nativeLiveInfo.keepOwnerOnSeat = liveInfo.keepOwnerOnSeat ?? false
nativeLiveInfo.maxSeatCount = liveInfo.maxSeatCount?.toInt() ?? 0
nativeLiveInfo.seatMode = ParamsCovert.convertTakeSeatMode(liveInfo.seatMode)
nativeLiveInfo.seatLayoutTemplateID = liveInfo.seatLayoutTemplateID?.toInt() ?? 600
nativeLiveInfo.coverURL = liveInfo.coverURL ?? ""
nativeLiveInfo.backgroundURL = liveInfo.backgroundURL ?? ""
let list = mutableListOf<Int>()
liveInfo.categoryList?.forEach((info : number) => {
list.add(info.toInt())
})
nativeLiveInfo.categoryList = list;
nativeLiveInfo.activityStatus = liveInfo.activityStatus?.toInt() ?? 0
nativeLiveInfo.totalViewerCount = liveInfo.totalViewerCount?.toInt() ?? 0
nativeLiveInfo.isGiftEnabled = liveInfo.isGiftEnabled ?? true
nativeLiveInfo.metaData = liveInfo.metaData ?? Map<string, string>()
return nativeLiveInfo
}
private static convertTakeSeatMode(seatMode ?: TakeSeatModeType) : TakeSeatMode {
if (seatMode == 'FREE') {
return TakeSeatMode.FREE
}
return TakeSeatMode.APPLY
}
private static convertModifyFlag(flag : LiveModifyFlag) : LiveInfo.ModifyFlag {
switch (flag) {
case 'LIVE_NAME':
return LiveInfo.ModifyFlag.LIVE_NAME
case 'NOTICE':
return LiveInfo.ModifyFlag.NOTICE
case 'IS_MESSAGE_DISABLE':
return LiveInfo.ModifyFlag.IS_MESSAGE_DISABLE
case 'IS_PUBLIC_VISIBLE':
return LiveInfo.ModifyFlag.IS_PUBLIC_VISIBLE
case 'SEAT_MODE':
return LiveInfo.ModifyFlag.SEAT_MODE
case 'COVER_URL':
return LiveInfo.ModifyFlag.COVER_URL
case 'BACKGROUND_URL':
return LiveInfo.ModifyFlag.BACKGROUND_URL
case 'CATEGORY_LIST':
return LiveInfo.ModifyFlag.CATEGORY_LIST
case 'ACTIVITY_STATUS':
return LiveInfo.ModifyFlag.ACTIVITY_STATUS
case 'SEAT_LAYOUT_TEMPLATE_ID':
return LiveInfo.ModifyFlag.SEAT_LAYOUT_TEMPLATE_ID
default:
return LiveInfo.ModifyFlag.NONE
}
}
public static convertModifyFlagList(modifyFlagList ?: LiveModifyFlag[]) : List<LiveInfo.ModifyFlag> {
let arrays = Array<LiveInfo.ModifyFlag>()
if (modifyFlagList != null && modifyFlagList.length > 0) {
modifyFlagList.forEach((flag : LiveModifyFlag) => {
const result = ParamsCovert.convertModifyFlag(flag)
arrays.push(result)
})
}
return arrays
}
public static convertMoveSeatPolicy(policyType ?: MoveSeatPolicyType) : MoveSeatPolicy {
switch (policyType) {
case 'FORCE_REPLACE':
return MoveSeatPolicy.FORCE_REPLACE;
case 'SWAP_POSITION':
return MoveSeatPolicy.SWAP_POSITION;
default:
return MoveSeatPolicy.ABORT_WHEN_OCCUPIED;
}
}
public static covertVideoQuality(quality : VideoQualityType) : VideoQuality {
switch (quality) {
case 'VIDEOQUALITY_540P':
return VideoQuality.QUALITY_540P; // 标清540P
case 'VIDEOQUALITY_720P':
return VideoQuality.QUALITY_720P; // 高清720P
case 'VIDEOQUALITY_1080P':
return VideoQuality.QUALITY_1080P; // 超清1080P
default:
return VideoQuality.QUALITY_360P; // value = 1 低清360P
}
}
public static convertBarrage(barrageParam : AppendLocalTipOptions) : Barrage {
const nativeBarrage = new Barrage()
nativeBarrage.liveID = barrageParam.liveID
nativeBarrage.sender = ParamsCovert.convertLiveUserInfo(barrageParam.sender)
nativeBarrage.sequence = barrageParam.sequence?.toLong() ?? 0
nativeBarrage.timestampInSecond = barrageParam.timestampInSecond?.toLong() ?? 0
nativeBarrage.messageType = ParamsCovert.convertMessageType(barrageParam.messageType)
nativeBarrage.textContent = barrageParam.textContent ?? ""
nativeBarrage.extensionInfo = barrageParam.extensionInfo ?? Map<string, string>()
nativeBarrage.businessID = barrageParam.businessID ?? ""
nativeBarrage.data = barrageParam.data ?? ""
return nativeBarrage
}
private static convertLiveUserInfo(info : LiveUserInfoParam) : LiveUserInfo {
let liveUseInfo = new LiveUserInfo()
liveUseInfo.userID = info?.userID ?? ""
liveUseInfo.userName = info?.userName ?? ""
liveUseInfo.avatarURL = info?.avatarURL ?? ""
return liveUseInfo
}
private static convertMessageType(messageType ?: MessageType) : BarrageType {
if (messageType == 'CUSTOM') {
return BarrageType.CUSTOM
}
return BarrageType.TEXT
}
public static convertAudioChangerType(changerType : AudioChangerTypeParam) : AudioChangerType {
switch (changerType) {
case 'CHILD':
return AudioChangerType.CHILD;
case 'LITTLE_GIRL':
return AudioChangerType.LITTLE_GIRL;
case 'MAN':
return AudioChangerType.MAN;
case 'HEAVY_METAL':
return AudioChangerType.HEAVY_METAL;
case 'COLD':
return AudioChangerType.COLD;
case 'FOREIGNER':
return AudioChangerType.FOREIGNER;
case 'TRAPPED_BEAST':
return AudioChangerType.TRAPPED_BEAST;
case 'FATSO':
return AudioChangerType.FATSO;
case 'STRONG_CURRENT':
return AudioChangerType.STRONG_CURRENT;
case 'HEAVY_MACHINERY':
return AudioChangerType.HEAVY_MACHINERY;
case 'ETHEREAL':
return AudioChangerType.ETHEREAL;
default:
return AudioChangerType.NONE;
}
}
public static convertAudioReverbType(reverbType : AudioReverbTypeParam) : AudioReverbType {
switch (reverbType) {
case 'KTV':
return AudioReverbType.KTV;
case 'SMALL_ROOM':
return AudioReverbType.SMALL_ROOM;
case 'AUDITORIUM':
return AudioReverbType.AUDITORIUM;
case 'DEEP':
return AudioReverbType.DEEP;
case 'LOUD':
return AudioReverbType.LOUD;
case 'METALLIC':
return AudioReverbType.METALLIC;
case 'MAGNETIC':
return AudioReverbType.MAGNETIC;
default:
return AudioReverbType.NONE;
}
}
public static convertJsonToUTSJSONObject(jsonData ?: string): UTSJSONObject {
let jsonObject = new UTSJSONObject();
if (jsonData == null) {
return jsonObject;
}
try {
jsonObject = JSON.parse(jsonData) as UTSJSONObject;
} catch (error) {
console.error('JSON parse failed:', error);
}
return jsonObject
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>AtomicXCore.framework/AtomicXCore</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>AtomicXCore.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>AtomicXCore.framework/AtomicXCore</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>AtomicXCore.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,622 @@
#if 0
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
#ifndef ATOMICXCORE_SWIFT_H
#define ATOMICXCORE_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
@import CoreFoundation;
@import Foundation;
@import ObjectiveC;
@import RTCRoomEngine;
@import UIKit;
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="AtomicXCore",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
@class TUIBattleInfo;
@class NSString;
@class TUIBattleUser;
SWIFT_PROTOCOL("_TtP11AtomicXCore14BattleObserver_") SWIFT_DEPRECATED
@protocol BattleObserver
- (void)onBattleStartedWithBattleInfo:(TUIBattleInfo * _Nonnull)battleInfo;
- (void)onBattleEndedWithBattleInfo:(TUIBattleInfo * _Nonnull)battleInfo;
- (void)onUserJoinBattleWithBattleId:(NSString * _Nonnull)battleId battleUser:(TUIBattleUser * _Nonnull)battleUser;
- (void)onUserExitBattleWithBattleId:(NSString * _Nonnull)battleId battleUser:(TUIBattleUser * _Nonnull)battleUser;
- (void)onBattleScoreChangedWithBattleId:(NSString * _Nonnull)battleId battleUserList:(NSArray<TUIBattleUser *> * _Nonnull)battleUserList;
- (void)onBattleRequestReceivedWithBattleId:(NSString * _Nonnull)battleId inviter:(TUIBattleUser * _Nonnull)inviter invitee:(TUIBattleUser * _Nonnull)invitee;
- (void)onBattleRequestCancelledWithBattleId:(NSString * _Nonnull)battleId inviter:(TUIBattleUser * _Nonnull)inviter invitee:(TUIBattleUser * _Nonnull)invitee;
- (void)onBattleRequestTimeoutWithBattleId:(NSString * _Nonnull)battleId inviter:(TUIBattleUser * _Nonnull)inviter invitee:(TUIBattleUser * _Nonnull)invitee;
- (void)onBattleRequestAcceptWithBattleId:(NSString * _Nonnull)battleId inviter:(TUIBattleUser * _Nonnull)inviter invitee:(TUIBattleUser * _Nonnull)invitee;
- (void)onBattleRequestRejectWithBattleId:(NSString * _Nonnull)battleId inviter:(TUIBattleUser * _Nonnull)inviter invitee:(TUIBattleUser * _Nonnull)invitee;
@end
SWIFT_CLASS("_TtC11AtomicXCore19BattleUserViewModel") SWIFT_DEPRECATED
@interface BattleUserViewModel : NSObject
@property (nonatomic, strong) TUIBattleUser * _Nonnull battleUser;
@property (nonatomic) CGRect rect;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
SWIFT_CLASS("_TtC11AtomicXCore15C2CSettingStore")
@interface C2CSettingStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
SWIFT_CLASS("_TtC11AtomicXCore13CallListStore")
@interface CallListStore : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
SWIFT_CLASS("_TtC11AtomicXCore17CallListStoreImpl")
@interface CallListStoreImpl : CallListStore
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
@class TUICallObserverExtraInfo;
@interface CallListStoreImpl (SWIFT_EXTENSION(AtomicXCore)) <TUICallObserver>
- (void)onCallReceived:(NSString * _Nonnull)callId callerId:(NSString * _Nonnull)callerId calleeIdList:(NSArray<NSString *> * _Nonnull)calleeIdList mediaType:(TUICallMediaType)mediaType info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallBegin:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallEnd:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType reason:(TUICallEndReason)reason userId:(NSString * _Nonnull)userId totalTime:(float)totalTime info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallNotConnected:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType reason:(TUICallEndReason)reason userId:(NSString * _Nonnull)userId info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallCancelled:(NSString * _Nonnull)callerId;
@end
SWIFT_CLASS("_TtC11AtomicXCore20CallParticipantStore")
@interface CallParticipantStore : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
SWIFT_CLASS("_TtC11AtomicXCore24CallParticipantStoreImpl")
@interface CallParticipantStoreImpl : CallParticipantStore
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
@class NSNumber;
@class TUINetworkQualityInfo;
@interface CallParticipantStoreImpl (SWIFT_EXTENSION(AtomicXCore)) <TUICallObserver>
- (void)onCallReceived:(NSString * _Nonnull)callId callerId:(NSString * _Nonnull)callerId calleeIdList:(NSArray<NSString *> * _Nonnull)calleeIdList mediaType:(TUICallMediaType)mediaType info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallBegin:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallEnd:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType reason:(TUICallEndReason)reason userId:(NSString * _Nonnull)userId totalTime:(float)totalTime info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallNotConnected:(NSString * _Nonnull)callId mediaType:(TUICallMediaType)mediaType reason:(TUICallEndReason)reason userId:(NSString * _Nonnull)userId info:(TUICallObserverExtraInfo * _Nonnull)info;
- (void)onCallCancelled:(NSString * _Nonnull)callerId;
- (void)onUserJoin:(NSString * _Nonnull)userId;
- (void)onUserLeave:(NSString * _Nonnull)userId;
- (void)onUserReject:(NSString * _Nonnull)userId;
- (void)onUserInviting:(NSString * _Nonnull)userId;
- (void)onUserLineBusy:(NSString * _Nonnull)userId;
- (void)onUserNoResponse:(NSString * _Nonnull)userId;
- (void)onUserVoiceVolumeChanged:(NSDictionary<NSString *, NSNumber *> * _Nonnull)volumeMap;
- (void)onUserNetworkQualityChanged:(NSArray<TUINetworkQualityInfo *> * _Nonnull)networkQualityList;
- (void)onUserAudioAvailable:(NSString * _Nonnull)userId isAudioAvailable:(BOOL)isAudioAvailable;
- (void)onUserVideoAvailable:(NSString * _Nonnull)userId isVideoAvailable:(BOOL)isVideoAvailable;
- (void)onKickedOffline;
@end
@class TUIConnectionUser;
SWIFT_CLASS("_TtC11AtomicXCore10CoHostUser") SWIFT_DEPRECATED
@interface CoHostUser : NSObject
@property (nonatomic, strong) TUIConnectionUser * _Nonnull connectionUser;
@property (nonatomic) BOOL hasAudioStream;
@property (nonatomic) BOOL hasVideoStream;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
@class TUIUserInfo;
/// ************************************** observer ***************************************
SWIFT_PROTOCOL("_TtP11AtomicXCore18ConnectionObserver_") SWIFT_DEPRECATED
@protocol ConnectionObserver
- (void)onConnectedUsersUpdatedWithUserList:(NSArray<TUIUserInfo *> * _Nonnull)userList joinList:(NSArray<TUIUserInfo *> * _Nonnull)joinList leaveList:(NSArray<TUIUserInfo *> * _Nonnull)leaveList;
- (void)onUserConnectionRequestWithInviterUser:(TUIUserInfo * _Nonnull)inviterUser;
- (void)onUserConnectionCancelledWithInviterUser:(TUIUserInfo * _Nonnull)inviterUser;
- (void)onUserConnectionAcceptedWithUserInfo:(TUIUserInfo * _Nonnull)userInfo;
- (void)onUserConnectionRejectedWithUserInfo:(TUIUserInfo * _Nonnull)userInfo;
- (void)onUserConnectionTimeoutWithUserInfo:(TUIUserInfo * _Nonnull)userInfo;
- (void)onUserConnectionTerminated;
- (void)onUserConnectionExitedWithUserInfo:(TUIUserInfo * _Nonnull)userInfo;
- (void)onConnectedRoomsUpdatedWithHostUserList:(NSArray<TUIConnectionUser *> * _Nonnull)hostUserList;
- (void)onCrossRoomConnectionRequestWithHostUser:(TUIConnectionUser * _Nonnull)hostUser;
- (void)onCrossRoomConnectionCancelledWithHostUser:(TUIConnectionUser * _Nonnull)hostUser;
- (void)onCrossRoomConnectionAcceptedWithHostUser:(TUIConnectionUser * _Nonnull)hostUser;
- (void)onCrossRoomConnectionRejectedWithHostUser:(TUIConnectionUser * _Nonnull)hostUser;
- (void)onCrossRoomConnectionTimeoutWithInviter:(TUIConnectionUser * _Nonnull)inviter invitee:(TUIConnectionUser * _Nonnull)invitee;
- (void)onCrossRoomConnectionExitedWithHostUser:(TUIConnectionUser * _Nonnull)hostUser;
- (void)onRoomDismissedWithRoomId:(NSString * _Nonnull)roomId;
@end
SWIFT_CLASS("_TtC11AtomicXCore16ContactListStore")
@interface ContactListStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
SWIFT_CLASS("_TtC11AtomicXCore21ConversationListStore")
@interface ConversationListStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
SWIFT_CLASS("_TtC11AtomicXCore17GroupSettingStore")
@interface GroupSettingStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
typedef SWIFT_ENUM(NSInteger, LayoutMode, open) {
LayoutModeGridLayout = 0,
LayoutModeFloatLayout = 1,
LayoutModeFreeLayout = 2,
};
@class NSCoder;
SWIFT_CLASS("_TtC11AtomicXCore12LiveCoreView")
@interface LiveCoreView : UIView
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder SWIFT_UNAVAILABLE;
- (nonnull instancetype)initWithFrame:(CGRect)frame SWIFT_UNAVAILABLE;
@end
@interface LiveCoreView (SWIFT_EXTENSION(AtomicXCore))
- (void)didMoveToWindow;
@end
/// Login management class.
SWIFT_CLASS("_TtC11AtomicXCore10LoginStore")
@interface LoginStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
SWIFT_CLASS("_TtC11AtomicXCore16MessageListStore")
@interface MessageListStore : NSObject
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
SWIFT_CLASS("_TtC11AtomicXCore15ParticipantView")
@interface ParticipantView : UIView
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder SWIFT_UNAVAILABLE;
- (nonnull instancetype)initWithFrame:(CGRect)frame SWIFT_UNAVAILABLE;
@end
@interface ParticipantView (SWIFT_EXTENSION(AtomicXCore))
- (void)didMoveToWindow;
@end
SWIFT_CLASS("_TtC11AtomicXCore15SignalVideoView")
@interface SignalVideoView : UIView
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder SWIFT_UNAVAILABLE;
- (void)didMoveToWindow;
- (nonnull instancetype)initWithFrame:(CGRect)frame SWIFT_UNAVAILABLE;
@end
SWIFT_CLASS("_TtC11AtomicXCore18UserInfoModifyFlag") SWIFT_DEPRECATED
@interface UserInfoModifyFlag : NSObject
SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly, strong) UserInfoModifyFlag * _Nonnull none;)
+ (UserInfoModifyFlag * _Nonnull)none SWIFT_WARN_UNUSED_RESULT;
SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly, strong) UserInfoModifyFlag * _Nonnull userRole;)
+ (UserInfoModifyFlag * _Nonnull)userRole SWIFT_WARN_UNUSED_RESULT;
SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly, strong) UserInfoModifyFlag * _Nonnull nameCard;)
+ (UserInfoModifyFlag * _Nonnull)nameCard SWIFT_WARN_UNUSED_RESULT;
SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly, strong) UserInfoModifyFlag * _Nonnull hasVideoStream;)
+ (UserInfoModifyFlag * _Nonnull)hasVideoStream SWIFT_WARN_UNUSED_RESULT;
SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly, strong) UserInfoModifyFlag * _Nonnull hasAudioStream;)
+ (UserInfoModifyFlag * _Nonnull)hasAudioStream SWIFT_WARN_UNUSED_RESULT;
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
@end
@class TUISeatFullInfo;
enum ViewLayer : NSInteger;
/// Video view adapter protocol
SWIFT_PROTOCOL("_TtP11AtomicXCore17VideoViewDelegate_")
@protocol VideoViewDelegate
/// Create co-guest view.
/// @param seatInfo User information.
/// @param viewLayer View layer. {@link ViewLayer}
/// @return Co-guest view.
- (UIView * _Nullable)createCoGuestViewWithSeatInfo:(TUISeatFullInfo * _Nonnull)seatInfo viewLayer:(enum ViewLayer)viewLayer SWIFT_WARN_UNUSED_RESULT;
/// Create cross-room co-host view.
/// @param seatInfo Cross-room co-host user information.
/// @param viewLayer View layer.
/// @return Cross-room co-host view.
- (UIView * _Nullable)createCoHostViewWithSeatInfo:(TUISeatFullInfo * _Nonnull)seatInfo viewLayer:(enum ViewLayer)viewLayer SWIFT_WARN_UNUSED_RESULT;
/// Create PK view.
/// @param battleUser PK user information.
/// @return PK view.
- (UIView * _Nullable)createBattleViewWithBattleUser:(TUIBattleUser * _Nonnull)battleUser SWIFT_WARN_UNUSED_RESULT;
/// Create PK container view.
/// @return PK container view.
- (UIView * _Nullable)createBattleContainerView SWIFT_WARN_UNUSED_RESULT;
@end
/// View layer.
typedef SWIFT_ENUM(NSInteger, ViewLayer, open) {
/// Foreground layer.
ViewLayerForeground = 0,
/// Background layer.
ViewLayerBackground = 1,
};
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#else
#error unsupported Swift architecture
#endif

View File

@@ -0,0 +1,16 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double AtomicXCoreVersionNumber;
FOUNDATION_EXPORT const unsigned char AtomicXCoreVersionString[];

View File

@@ -0,0 +1,11 @@
framework module AtomicXCore {
umbrella header "AtomicXCore-umbrella.h"
export *
module * { export * }
}
module AtomicXCore.Swift {
header "AtomicXCore-Swift.h"
requires objc
}

View File

@@ -0,0 +1,16 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double AtomicXCoreVersionNumber;
FOUNDATION_EXPORT const unsigned char AtomicXCoreVersionString[];

View File

@@ -0,0 +1,11 @@
framework module AtomicXCore {
umbrella header "AtomicXCore-umbrella.h"
export *
module * { export * }
}
module AtomicXCore.Swift {
header "AtomicXCore-Swift.h"
requires objc
}

View File

@@ -0,0 +1,297 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/AtomicXCore-Swift.h</key>
<data>
0dmswbw3RWzDigjAYHUzaqL4CN4=
</data>
<key>Headers/AtomicXCore-umbrella.h</key>
<data>
g7gcVChc6M2o9SyWmZ5d3DT5Bck=
</data>
<key>Info.plist</key>
<data>
pxyKul0PUz96X+xK4PlbTCR+4hw=
</data>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<data>
d4eG79RPvGY6Xoq1433e8iiLKa8=
</data>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<data>
ZFdJRfcLH5xQSS+YkWAm603b6f8=
</data>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<data>
ScLj6E3f03mEQ40uJ5f9cAPQ0y0=
</data>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<data>
ZFdJRfcLH5xQSS+YkWAm603b6f8=
</data>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<data>
dHg9opx4H9kXDQ00K0UwzpYY5CE=
</data>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<data>
d4eG79RPvGY6Xoq1433e8iiLKa8=
</data>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<data>
U/JvN8jBiCb9ZGAnBJgmL+BzAzM=
</data>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<data>
GZTbYyi8tDZ3nHCB3qfQQsPOTos=
</data>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<data>
U/JvN8jBiCb9ZGAnBJgmL+BzAzM=
</data>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<data>
Eu1zT3FuFh0lXuRynTnoudL9A+U=
</data>
<key>Modules/module.modulemap</key>
<data>
CVIbcqidHGDTUSq8lpk9bR3T1AM=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/AtomicXCore-Swift.h</key>
<dict>
<key>hash</key>
<data>
0dmswbw3RWzDigjAYHUzaqL4CN4=
</data>
<key>hash2</key>
<data>
HjShXRLFtfDVz3QEKugubHKSpfIrLG2s66u9zANYp5s=
</data>
</dict>
<key>Headers/AtomicXCore-umbrella.h</key>
<dict>
<key>hash</key>
<data>
g7gcVChc6M2o9SyWmZ5d3DT5Bck=
</data>
<key>hash2</key>
<data>
tOAe4tsjHYbEaMSLYEkve2xtuM2VnFqc14CIWt4xdSg=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash</key>
<data>
d4eG79RPvGY6Xoq1433e8iiLKa8=
</data>
<key>hash2</key>
<data>
o/AA86qNw/GWgAoo5ALJZizPlFQkHX4v3ZPMqg5bXeI=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash</key>
<data>
ZFdJRfcLH5xQSS+YkWAm603b6f8=
</data>
<key>hash2</key>
<data>
aw3+kcmgy1AFWkAzaiR1g/GEmiZwM/MQYlNJr15J+OM=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash</key>
<data>
ScLj6E3f03mEQ40uJ5f9cAPQ0y0=
</data>
<key>hash2</key>
<data>
wFfcV/YiIWIHZT5VLxXhMdfK9BYJ6hXM7r7u0/nKoi8=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash</key>
<data>
ZFdJRfcLH5xQSS+YkWAm603b6f8=
</data>
<key>hash2</key>
<data>
aw3+kcmgy1AFWkAzaiR1g/GEmiZwM/MQYlNJr15J+OM=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash</key>
<data>
dHg9opx4H9kXDQ00K0UwzpYY5CE=
</data>
<key>hash2</key>
<data>
SHFHqRPyCKNzZ3TWjoEOGErMzM1KzQbMW2XGG3MSUb4=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
<dict>
<key>hash</key>
<data>
d4eG79RPvGY6Xoq1433e8iiLKa8=
</data>
<key>hash2</key>
<data>
o/AA86qNw/GWgAoo5ALJZizPlFQkHX4v3ZPMqg5bXeI=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
<dict>
<key>hash</key>
<data>
U/JvN8jBiCb9ZGAnBJgmL+BzAzM=
</data>
<key>hash2</key>
<data>
Ed7kOBklhWMpOoIrwE8CDK5jUF4QFWnzwbr3RTLd0/U=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
<dict>
<key>hash</key>
<data>
GZTbYyi8tDZ3nHCB3qfQQsPOTos=
</data>
<key>hash2</key>
<data>
i30Kt2XbXkE4BfZ6N3EVI3+DCLF3IoZUA7Lth8rHD/k=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
<dict>
<key>hash</key>
<data>
U/JvN8jBiCb9ZGAnBJgmL+BzAzM=
</data>
<key>hash2</key>
<data>
Ed7kOBklhWMpOoIrwE8CDK5jUF4QFWnzwbr3RTLd0/U=
</data>
</dict>
<key>Modules/AtomicXCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
<dict>
<key>hash</key>
<data>
Eu1zT3FuFh0lXuRynTnoudL9A+U=
</data>
<key>hash2</key>
<data>
FDKiMHuUvc4Yp1qQl5VNoxUYtYO78efLYbSZ/hXeItA=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash</key>
<data>
CVIbcqidHGDTUSq8lpk9bR3T1AM=
</data>
<key>hash2</key>
<data>
1Y5XpYnPrQ9YxnJG/+tUEQeOswjkdJAJnFTedr3m+TA=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>RTCRoomEngine.framework/RTCRoomEngine</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>RTCRoomEngine.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>RTCRoomEngine.framework/RTCRoomEngine</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_armv7</string>
<key>LibraryPath</key>
<string>RTCRoomEngine.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>armv7</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,204 @@
/**
* Copyright (c) 2024 Tencent. All rights reserved.
*/
#import "TUIRoomEngine.h"
@class TRTCCloud;
@class TXDeviceManager;
@class TXAudioEffectManager;
@class TXBeautyManager;
NS_ASSUME_NONNULL_BEGIN
@interface TUIRoomEngine (deprecated)
/////////////////////////////////////////////////////////////////////////////////
//
// 弃用接口(建议使用对应的新接口)
//
/////////////////////////////////////////////////////////////////////////////////
/**
* 获得设备管理对象
*
* @deprecated v1.5.0 版本开始不推荐使用。
*/
- (TXDeviceManager *)getDeviceManager NS_SWIFT_NAME(getDeviceManager())
__attribute__((deprecated("Deprecated from v1.5.0")));
/**
* 获得音效管理对象
*
* @deprecated v1.5.0 版本开始不推荐使用。
*/
- (TXAudioEffectManager *)getAudioEffectManager NS_SWIFT_NAME(getAudioEffectManager())
__attribute__((deprecated("Deprecated from v1.5.0")));
/**
* 获得美颜管理对象
*
* @deprecated v1.5.0 版本开始不推荐使用。
*/
- (TXBeautyManager *)getBeautyManager NS_SWIFT_NAME(getBeautyManager())
__attribute__((deprecated("Deprecated from v1.5.0")));
/**
* 设置本地用户视频渲染的视图控件
*
* @deprecated v1.6.1 版本开始不推荐使用。
*/
- (void)setLocalVideoView:(TUIVideoStreamType)streamType
view:(TUIVideoView *__nullable)view
NS_SWIFT_NAME(setLocalVideoView(streamType:view:))
__attribute__((deprecated("Deprecated from v1.6.1")));
#if TARGET_OS_IPHONE
/**
* 切换前置或后置摄像头(仅适用于移动端)
*
* @deprecated v2.0 版本开始不推荐使用,建议使用{$TUIRoomDeviceManager$}中的{@link
* switchCamera}代替。
* @return 0操作成功负数操作失败。
*/
- (NSInteger)switchCamera:(BOOL)frontCamera NS_SWIFT_NAME(switchCamera(frontCamera:))
__attribute__((deprecated("Deprecated from v2.0")));
#endif
#if !TARGET_OS_IPHONE && TARGET_OS_MAC
/**
* 获取设备列表(仅适用于桌面端)
*
* @deprecated v2.0 版本开始不推荐使用,建议使用{$TUIRoomDeviceManager$}中的{@link
* getDevicesList}代替。
* @param type 设备类型,指定需要获取哪种设备的列表。详见 TXMediaDeviceType 定义。
* @note
* - 使用完毕后请调用 release 方法释放资源,这样可以让 SDK 维护 ITXDeviceCollection 对象的生命周期。
* - 不要使用 delete 释放返回的 Collection 对象delete ITXDeviceCollection* 指针会导致异常崩溃。
* - type 只支持 TXMediaDeviceTypeMic、TXMediaDeviceTypeSpeaker、TXMediaDeviceTypeCamera。
* - 此接口只支持 Mac 和 Windows 平台。
*/
- (NSArray<TXMediaDeviceInfo *> *_Nullable)getDevicesList:(TUIMediaDeviceType)type
NS_SWIFT_NAME(getDevicesList(type:)) __attribute__((deprecated("Deprecated from v2.0")));
/**
* 设置当前要使用的设备(仅适用于桌面端)
*
* @deprecated v2.0 版本开始不推荐使用,建议使用{$TUIRoomDeviceManager$}中的{@link
* setCurrentDevice}代替。 设置当前要使用的设备后,SDK会通过 {@link TUIRoomObserver} 中的 {@link
* onDeviceChanged} 通知您。
* @param type 设备类型,详见 TXMediaDeviceType 定义。
* @param deviceId 设备ID您可以通过接口 {@link getDevicesList} 获得设备 ID。
* @return 0操作成功负数操作失败。
*/
- (NSInteger)setCurrentDevice:(TUIMediaDeviceType)type
deviceId:(NSString *)deviceId NS_SWIFT_NAME(setCurrentDevice(type:deviceId:))
__attribute__((deprecated("Deprecated from v2.0")));
#endif
/**
* 调用实验性接口
*
* @deprecated v3.0 版本开始不推荐使用
* @note 此函数支持会议房间类型和直播房间类型({@link TUIRoomTypeConference} & {@link
* TUIRoomTypeLive})。
* @param jsonStr 接口信息。
* @return 返回结果
*/
+ (id)callExperimentalAPI:(NSString *)jsonStr NS_SWIFT_NAME(callExperimentalAPI(jsonStr:))
__attribute__((deprecated("Deprecated from v3.0")));
/**
* 上麦(上麦发言模式下,需要申请)
*
* @deprecated v3.5 版本开始不推荐使用
* @note 此函数支持会议房间类型和直播房间类型({@link TUIRoomTypeConference} & {@link
* TUIRoomTypeLive})。 上麦成功后,SDK会通过 {@link TUIRoomObserver} 中的 {@link onSeatListChanged}
* 通知房间内用户。
* @note 开启上麦发言模式时,需要向主持人或管理员发起申请才允许上麦。
* 开启自由发言模式,直播场景可以自由上麦,上麦后开麦发言,会议场景无需调用该接口,即可开麦发言。
* @param seatIndex 麦位编号。未开启麦位、不关心麦位序列的情况下,填-1即可。
* @param timeout 超时时间,单位秒,如果设置为 0SDK 不会做超时检测,也不会触发超时回调。
* @param onAccepted 邀请被接受的回调。
* @param onRejected 邀请被拒绝的回调。
* @param onCancelled 邀请被取消的回调。
* @param onTimeout 邀请超时未处理的回调。
* @param onError 邀请发生错误的回调。
* @return TUIRequest 请求体。
*/
- (TUIRequest *)takeSeat:(NSInteger)seatIndex
timeout:(NSTimeInterval)timeout
onAccepted:(TUIRequestAcceptedBlock)onAccepted
onRejected:(TUIRequestRejectedBlock)onRejected
onCancelled:(TUIRequestCancelledBlock)onCancelled
onTimeout:(TUIRequestTimeoutBlock)onTimeout
onError:(TUIRequestErrorBlock)onError
NS_SWIFT_NAME(takeSeat(_:timeout:onAccepted:onRejected:onCancelled:onTimeout:onError:));
/**
* 主持人/管理员 邀请用户上麦
*
* @deprecated v3.5 版本开始不推荐使用
* @note 此函数支持会议房间类型和直播房间类型({@link TUIRoomTypeConference} & {@link
* TUIRoomTypeLive})。 接口调用成功后,SDK会通过 {@link TUIRoomObserver} 中的 {@link
* onRequestReceived} 通知被邀请用户。
* @param seatIndex 麦位编号。未开启麦位、不关心麦位序列的情况下,填-1即可。
* @param userId 用户ID。
* @param timeout 超时时间,单位秒,如果设置为 0SDK 不会做超时检测,也不会触发超时回调。
* @param onAccepted 邀请被接受的回调。
* @param onRejected 邀请被拒绝的回调。
* @param onCancelled 邀请被取消的回调。
* @param onTimeout 邀请超时未处理的回调。
* @param onError 邀请发生错误的回调。
* @return TUIRequest 请求体。
*/
- (TUIRequest *)takeUserOnSeatByAdmin:(NSInteger)seatIndex
userId:(NSString *)userId
timeout:(NSTimeInterval)timeout
onAccepted:(TUIRequestAcceptedBlock)onAccepted
onRejected:(TUIRequestRejectedBlock)onRejected
onCancelled:(TUIRequestCancelledBlock)onCancelled
onTimeout:(TUIRequestTimeoutBlock)onTimeout
onError:(TUIRequestErrorBlock)onError
NS_SWIFT_NAME(takeUserOnSeatByAdmin(_:userId:timeout:onAccepted:onRejected:onCancelled:onTimeout:onError:));
/**
* 主持人/管理员 将用户踢下麦
*
* @deprecated v3.5 版本开始不推荐使用
* @note 此函数支持会议房间类型和直播房间类型({@link TUIRoomTypeConference} & {@link
* TUIRoomTypeLive})。 接口调用成功后,SDK会通过 {@link TUIRoomObserver} 中的 {@link
* onSeatListChanged} 通知房间内用户。
* @param seatIndex 麦位编号。未开启麦位、不关心麦位序列的情况下,填-1即可。
* @param userId 用户ID。
* @param onSuccess 成功回调。
* @param onError 失败回调。
*/
- (void)kickUserOffSeatByAdmin:(NSInteger)seatIndex
userId:(NSString *)userId
onSuccess:(TUISuccessBlock)onSuccess
onError:(TUIErrorBlock)onError
NS_SWIFT_NAME(kickUserOffSeatByAdmin(_:userId:onSuccess:onError:));
/**
* 回复请求
*
* @deprecated v3.5 版本开始不推荐使用
* @note 此函数支持会议房间类型和直播房间类型({@link TUIRoomTypeConference} & {@link
* TUIRoomTypeLive})。
* @note 在收到信令请求时,可以使用此接口来回复接收到的请求。
* @param requestId 请求ID(发送请求的接口返回或者OnRequestReceived事件通知)。
* @param agree 是否同意 YES: 同意请求, NO: 拒绝请求。
* @param onSuccess 成功回调。
* @param onError 失败回调。
*/
- (void)responseRemoteRequest:(NSString *)requestId
agree:(BOOL)agree
onSuccess:(TUISuccessBlock)onSuccess
onError:(TUIErrorBlock)onError
NS_SWIFT_NAME(responseRemoteRequest(_:agree:onSuccess:onError:));
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Tencent. All Rights Reserved.
*
*/
#import <RTCRoomEngine/TUICommonDefine.h>
#import <RTCRoomEngine/TUIEngineSymbolExport.h>
#import <RTCRoomEngine/TUIRoomDeviceManager.h>
#import <RTCRoomEngine/TUILiveListManager.h>
#import <RTCRoomEngine/TUILiveConnectionManager.h>
#import <RTCRoomEngine/TUILiveBattleManager.h>
#import <RTCRoomEngine/TUILiveLayoutManager.h>
#import <RTCRoomEngine/TUILiveGiftManager.h>
#import <RTCRoomEngine/TUIConferenceListManager.h>
#import <RTCRoomEngine/TUIConferenceInvitationManager.h>
#import <RTCRoomEngine/DeprecatedRoomEngineAPI.h>
#import <RTCRoomEngine/TUIRoomEngine.h>
#import <RTCRoomEngine/TUIRoomDefine.h>
#import <RTCRoomEngine/TUIRoomObserver.h>
#import <RTCRoomEngine/TUICallEngine.h>
#import <RTCRoomEngine/TUICallDefine.h>
#import <RTCRoomEngine/TUICallObserver.h>

View File

@@ -0,0 +1,217 @@
/**
* Copyright (c) 2022 Tencent. All rights reserved.
* Module: TUICallDefine 关键类型定义
* Description: 定义枚举或常量,例如:通话类型,角色等。
*/
#import <Foundation/Foundation.h>
#import "TUICommonDefine.h"
@class TUIOfflinePushInfo;
typedef void (^TUICallSucc)(void);
typedef void (^TUICallFail)(int code, NSString *_Nullable errMsg);
/// TUICallEngine 版本号
static const NSString *_Nullable TUICALL_VERSION = @"0.0.0.0";
/// 您当前未购买音视频通话能力套餐请前往控制台开通免费体验https://console.cloud.tencent.com/im/detail或加购正式版https://buy.cloud.tencent.com/avc
static const int ERROR_PACKAGE_NOT_PURCHASED = -1001;
/// 您当前购买的套餐不支持该能力请先购买https://buy.cloud.tencent.com/avc
static const int ERROR_PACKAGE_NOT_SUPPORTED = -1002;
/// TIM SDK 版本过低,请升级 TIM SDK 版本 >= 6.6
static const int ERROR_TIM_VERSION_OUTDATED = -1003;
/// 获取权限失败,当前未授权音 / 视频权限,请查看是否开启设备权限
static const int ERROR_PERMISSION_DENIED = -1101;
/// 未调用 initTUICallEngine API 使用需在 init 之后
static const int ERROR_INIT_FAIL = -1201;
/// 参数错误
static const int ERROR_PARAM_INVALID = -1202;
/// 当前状态不支持调用
static const int ERROR_REQUEST_REFUSED = -1203;
/// 当前方法正在执行中,请勿重复调用
static const int ERROR_REQUEST_REPEATED = -1204;
/// 当前通话场景,不支持该功能
static const int ERROR_SCENE_NOT_SUPPORTED = -1205;
/// 信令发送失败
static const int ERROR_SIGNALING_SEND_FAIL = -1401;
/// 通话类型:未知、音频、视频
typedef NS_ENUM(NSUInteger, TUICallMediaType) {
TUICallMediaTypeUnknown,
TUICallMediaTypeAudio,
TUICallMediaTypeVideo,
};
/// 通话角色:未知、主叫、被叫
typedef NS_ENUM(NSUInteger, TUICallRole) {
TUICallRoleNone,
TUICallRoleCall,
TUICallRoleCalled,
};
/// 通话状态:空闲、等待中、接听中
typedef NS_ENUM(NSUInteger, TUICallStatus) {
TUICallStatusNone,
TUICallStatusWaiting,
TUICallStatusAccept,
};
typedef NS_ENUM(NSUInteger, TUICallScene) {
TUICallSceneNone = 0,
TUICallSceneGroup = 1,
TUICallSceneMulti = 2,
TUICallSceneSingle = 3,
};
typedef NS_ENUM(NSUInteger, TUICallIOSOfflinePushType) {
TUICallIOSOfflinePushTypeAPNs = 0,
TUICallIOSOfflinePushTypeVoIP = 1,
};
typedef NS_ENUM(NSUInteger, TUICallEndReason) {
TUICallEndReasonUnknown = 0,
TUICallEndReasonHangup = 1,
TUICallEndReasonReject = 2,
TUICallEndReasonNoResponse = 3,
TUICallEndReasonOffline = 4,
TUICallEndReasonLineBusy = 5,
TUICallEndReasonCanceled = 6,
TUICallEndReasonOtherDeviceAccepted = 7,
TUICallEndReasonOtherDeviceReject = 8,
TUICallEndReasonEndByServer = 9,
};
// 通话结果
typedef NS_ENUM(NSUInteger, TUICallResultType) {
TUICallResultTypeUnknown,
TUICallResultTypeMissed,
TUICallResultTypeIncoming,
TUICallResultTypeOutgoing,
};
/**
* 扩展参数
*/
NS_ASSUME_NONNULL_BEGIN
TUIENGINE_EXPORT @interface TUICallParams : NSObject
@property(nonatomic, strong) TUIRoomId *roomId;
@property(nonatomic, strong) TUIOfflinePushInfo *offlinePushInfo;
@property(nonatomic, assign) int timeout;
@property(nonatomic, copy) NSString *userData;
@property(nonatomic, copy) NSString *chatGroupId;
@property(nonatomic, assign) BOOL isEphemeralCall;
@end
/**
* 扩展信息
*/
TUIENGINE_EXPORT @interface TUICallObserverExtraInfo : NSObject
@property(nonatomic, strong) TUIRoomId *roomId;
@property(nonatomic, assign) TUICallRole role;
@property(nonatomic, copy) NSString *userData;
@property(nonatomic, copy) NSString *chatGroupId;
@property(nonatomic, assign) BOOL isEphemeralCall;
@end
/**
* 离线推送自定义类
*/
TUIENGINE_EXPORT @interface TUIOfflinePushInfo : NSObject
@property(nonatomic, copy) NSString *title;
@property(nonatomic, copy) NSString *desc;
@property(nonatomic, assign) BOOL ignoreIOSBadge;
@property(nonatomic, assign) BOOL enableIOSBackgroundNotification;
@property(nonatomic, copy) NSString *iOSInterruptionLevel;
@property(nonatomic, copy) NSString *iOSImage;
@property(nonatomic, copy) NSString *iOSSound;
@property(nonatomic, copy) NSString *AndroidSound;
@property(nonatomic, copy) NSString *AndroidOPPOChannelID;
@property(nonatomic, copy) NSString *OPPOCategory;
@property(nonatomic, assign) NSInteger OPPONotifyLevel;
@property(nonatomic, copy) NSString *AndroidFCMChannelID;
@property(nonatomic, copy) NSString *fcmImage;
@property(nonatomic, assign) NSInteger AndroidVIVOClassification;
@property(nonatomic, copy) NSString *VIVOCategory;
@property(nonatomic, copy) NSString *AndroidXiaoMiChannelID;
@property(nonatomic, copy) NSString *AndroidHuaWeiCategory;
@property(nonatomic, copy) NSString *huaweiImage;
@property(nonatomic, copy) NSString *honorImportance;
@property(nonatomic, copy) NSString *honorImage;
@property(nonatomic, assign) BOOL isDisablePush;
@property(nonatomic, assign) TUICallIOSOfflinePushType iOSPushType;
@property(nonatomic, copy) NSString *extraInfo;
@end
/**
* 通话记录
*/
TUIENGINE_EXPORT @interface TUICallRecords : NSObject
@property(nonatomic, copy) NSString *callId;
@property(nonatomic, copy) NSString *inviter;
@property(nonatomic, strong) NSArray *inviteList;
@property(nonatomic, copy) NSString *groupId;
@property(nonatomic, assign) TUICallScene scene;
@property(nonatomic, assign) TUICallMediaType mediaType;
@property(nonatomic, assign) TUICallRole role;
@property(nonatomic, assign) TUICallResultType result;
@property(nonatomic, assign) NSTimeInterval beginTime;
@property(nonatomic, assign) NSTimeInterval totalTime;
@end
/// 通话扩展参数
@interface TUICallRecentCallsFilter : NSObject
@property(nonatomic, assign) TUICallResultType result;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,282 @@
/**
* Copyright (c) 2022 Tencent. All rights reserved.
* Module: TUICallEngine
* Function: TUICallEngine's main feature API
* Version: <:Version:>
*/
#import <Foundation/Foundation.h>
#import "TUICallObserver.h"
#import "TUICallDefine.h"
#import "TUICommonDefine.h"
@class TRTCCloud;
NS_ASSUME_NONNULL_BEGIN
TUIENGINE_EXPORT @interface TUICallEngine : NSObject
/**
* 创建 TUICallEngine 实例(单例模式)
*/
+ (TUICallEngine *)createInstance NS_SWIFT_NAME(createInstance());
/**
* 销毁 TUICallEngine 实例(单例模式)
*/
+ (void)destroyInstance NS_SWIFT_NAME(destroyInstance());
/**
* 初始化函数,请在使用所有功能之前先调用该函数,以便完成包含通话服务鉴权在内初始化动作。
*
* @param sdkAppId 应用ID
* @param userId 当前用户的 ID
* @param userSig 安全保护签名
*/
- (void)init:(int)sdkAppId userId:(NSString *)userId userSig:(NSString *)userSig succ:(TUICallSucc)succ fail:(TUICallFail)fail;
/**
* 添加回调接口您可以通过这个接口监听TUICallObserver相关的事件回调。
*/
- (void)addObserver:(id<TUICallObserver>)observer NS_SWIFT_NAME(addObserver(_:));
/**
* 移除事件回调
*/
- (void)removeObserver:(id<TUICallObserver>)observer NS_SWIFT_NAME(removeObserver(_:));
/**
* 发起 1VN 通话
*
* @param userIdList 目标用户的 userId 列表
* @param callMediaType 通话的媒体类型,例如:视频通话、语音通话
* @param params 通话参数扩展字段,例如:离线推送自定义内容
*/
- (void)calls:(NSArray<NSString *> *)userIdList callMediaType:(TUICallMediaType)callMediaType params:(TUICallParams *_Nullable)params succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(calls(userIdList:callMediaType:params:succ:fail:));
/**
* 接受当前通话,当您作为被叫收到 onCallReceived() 的回调时,可以调用该函数接听来电。
*/
- (void)accept:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(accept(succ:fail:));
/**
* 拒绝当前通话,当您作为被叫收到 onCallReceived() 的回调时,可以调用该函数拒绝来电。
*/
- (void)reject:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(reject(succ:fail:));
/**
* 挂断当前通话,当您处于通话中,可以调用该函数结束通话。
*/
- (void)hangup:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(hangup(succ:fail:));
/**
* 忽略当前通话,当您作为被叫收到 onCallReceived() 的回调时,可以调用该函数忽略来电,此时主叫会收到 onUserLineBusy 的回调。
* 备注:如果您的业务中存在直播、会议等场景,在直播/会议中的情况时,也可以调用这个函数来忽略此次来电。
*/
- (void)ignore:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(ignore(succ:fail:));
/**
* 主动加入此次多人通话。
*
* @param callId 此次通话的唯一ID
*/
- (void)join:(NSString *)callId succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(join(callId:succ:fail:));
/**
* 邀请用户加入此次群组通话。
* 使用场景:一个群组通话中的用户主动邀请其他人时使用。
*
* @param userIdList 目标用户的 userId 列表
* @param params 通话参数扩展字段,例如:离线推送自定义内容
*/
- (void)inviteUser:(NSArray<NSString *> *)userIdList params:(TUICallParams *)params succ:(void (^)(NSArray<NSString *> *userIdList))succ fail:(TUICallFail)fail NS_SWIFT_NAME(inviteUser(userIdList:params:succ:fail:));
/**
* 切换通话媒体类型,比如视频通话切音频通话
*
* @param callMediaType 通话的媒体类型,例如:视频通话、语音通话
*/
- (void)switchCallMediaType:(TUICallMediaType)callMediaType NS_SWIFT_NAME(switchCallMediaType(_:));
/**
* 开始订阅远端用户视频流
*
* @param userId 目标用户的 userId
* @param videoView 待渲染的视图
*/
- (void)startRemoteView:(NSString *)userId
videoView:(TUIVideoView *)videoView
onPlaying:(void (^)(NSString *userId))onPlaying
onLoading:(void (^)(NSString *userId))onLoading
onError:(void (^)(NSString *userId, int code, NSString *errMsg))onError NS_SWIFT_NAME(startRemoteView(userId:videoView:onPlaying:onLoading:onError:));
/**
* 停止订阅远端用户视频流
*
* @param userId 目标用户的 userId
*/
- (void)stopRemoteView:(NSString *)userId NS_SWIFT_NAME(stopRemoteView(userId:));
/**
* 开启摄像头
*
* @param camera 前置/后置 摄像头
* @param videoView 待渲染的视图
*/
- (void)openCamera:(TUICamera)camera videoView:(TUIVideoView *)videoView succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(openCamera(_:videoView:succ:fail:));
/**
* 关闭摄像头
*/
- (void)closeCamera NS_SWIFT_NAME(closeCamera());
/**
* 切换前后摄像头
*
* @param camera 前置/后置 摄像头
*/
- (void)switchCamera:(TUICamera)camera NS_SWIFT_NAME(switchCamera(_:));
/**
* 打开麦克风
*/
- (void)openMicrophone:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(openMicrophone(succ:fail:));
/**
* 关闭麦克风
*/
- (void)closeMicrophone NS_SWIFT_NAME(closeMicrophone());
/**
* 选择音频播放设备(听筒/扬声器)
*
* @param device 听筒/扬声器
*/
- (void)selectAudioPlaybackDevice:(TUIAudioPlaybackDevice)device NS_SWIFT_NAME(selectAudioPlaybackDevice(_:));
/**
* 设置用户的昵称、头像
*
* @param nickname 用户昵称
* @param avatar 用户头像(格式为 URL
*/
- (void)setSelfInfo:(NSString *_Nullable)nickname avatar:(NSString *_Nullable)avatar succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(setSelfInfo(nickname:avatar:succ:fail:));
/**
* 开启/关闭 TUICallEngine 的多设备登录模式 (尊享版套餐支持)
*
* @param enable 开启:true; 关闭:false
*/
- (void)enableMultiDeviceAbility:(BOOL)enable succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(enableMultiDeviceAbility(enable:succ:fail:));
/**
* 设置用户画面的渲染模式
*
* @param userId 指定用户的 ID
* @param params 画面渲染参数:画面的旋转角度、填充模式,详见 {@link TUICommonDefine.VideoRenderParams}.
*/
- (void)setVideoRenderParams:(NSString *)userId params:(TUIVideoRenderParams *)params succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(setVideoRenderParams(userId:params:succ:fail:));
/**
* 设置视频编码器的编码参数
* 该设置能够决定远端用户看到的画面质量,同时也能决定云端录制出的视频文件的画面质量。
*
* @param params 编码参数:画面的分辨率、视频宽高比模式,详见 {@link TUICommonDefine.VideoEncoderParams}.
*/
- (void)setVideoEncoderParams:(TUIVideoEncoderParams *)params succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(setVideoEncoderParams(_:succ:fail:));
/**
* 设置美颜
*/
- (void)setBeautyLevel:(CGFloat)level succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(setBeautyLevel(_:succ:fail:));
/**
* 高级接口,获得当前通话业务中的 TRTCCloud 实例
*
* @return TRTCCloud 实例
*/
- (TRTCCloud *)getTRTCCloudInstance NS_SWIFT_NAME(getTRTCCloudInstance());
/**
* 调用实验性接口
*
* @param jsonObject json 数据
*/
- (void)callExperimentalAPI:(NSString *)jsonObject NS_SWIFT_NAME(callExperimentalAPI(jsonObject:));
/**
* 查询通话记录
*
* @param filter 查询过滤条件
*/
- (void)queryRecentCalls:(TUICallRecentCallsFilter *)filter succ:(void (^)(NSArray<TUICallRecords *> *callRecords))succ fail:(void (^)(void))fail NS_SWIFT_NAME(queryRecentCalls(filter:succ:fail:));
/**
* 删除通话记录
*
* @param callIdList 待删除记录的callId列表
*/
- (void)deleteRecordCalls:(NSArray<NSString *> *)callIdList succ:(void (^)(NSArray<NSString *> *succList))succ fail:(void (^)(void))fail NS_SWIFT_NAME(deleteRecordCalls(_:succ:fail:));
/**
* 设置视频通话虚化效果
*
* @param level 虚化等级: 0-关闭虚化效果; 1-low; 2-middle; 3-high
*/
- (void)setBlurBackground:(NSInteger)level fail:(TUICallFail)fail NS_SWIFT_NAME(setBlurBackground(_:fail:));
/**
* 设置视频通话图片背景
*
* @param imagePath 图片本地路径,为空则关闭背景图片效果; 不为空则开启图片背景效果
*/
- (void)setVirtualBackground:(NSString *)imagePath fail:(TUICallFail)fail NS_SWIFT_NAME(setVirtualBackground(_:fail:));
/**
* 此接口已于2023年9月废弃。若您刚刚开始接入TUICallEngine请优先使用calls其功能更强大稳定性更佳。
*/
- (void)call:(TUIRoomId *)roomId
userId:(NSString *)userId
callMediaType:(TUICallMediaType)callMediaType
params:(TUICallParams *)params
succ:(TUICallSucc)succ
fail:(TUICallFail)fail NS_SWIFT_NAME(call(roomId:userId:callMediaType:params:succ:fail:)) __attribute__((deprecated));
/**
* 此接口已于2025年1月废弃。若您刚刚开始接入TUICallEngine请优先使用calls其功能更强大稳定性更佳。
*/
- (void)call:(NSString *)userId callMediaType:(TUICallMediaType)callMediaType params:(TUICallParams *)params succ:(TUICallSucc)succ fail:(TUICallFail)fail NS_SWIFT_NAME(call(userId:callMediaType:params:succ:fail:)) __attribute__((deprecated));
/**
* 此接口已于2023年9月废弃。若您刚刚开始接入TUICallEngine请优先使用calls其功能更强大稳定性更佳。
*/
- (void)groupCall:(TUIRoomId *)roomId
groupId:(NSString *)groupId
userIdList:(NSArray<NSString *> *)userIdList
callMediaType:(TUICallMediaType)callMediaType
params:(TUICallParams *)params
succ:(TUICallSucc)succ
fail:(TUICallFail)fail NS_SWIFT_NAME(groupCall(roomId:groupId:userIdList:callMediaType:params:succ:fail:)) __attribute__((deprecated));
/**
* 此接口已于2025年1月废弃。若您刚刚开始接入TUICallEngine请优先使用calls其功能更强大稳定性更佳。
*/
- (void)groupCall:(NSString *)groupId
userIdList:(NSArray<NSString *> *)userIdList
callMediaType:(TUICallMediaType)callMediaType
params:(TUICallParams *)params
succ:(TUICallSucc)succ
fail:(TUICallFail)fail NS_SWIFT_NAME(groupCall(groupId:userIdList:callMediaType:params:succ:fail:)) __attribute__((deprecated));
/**
* 此接口已于2015年1月废弃。若您刚刚开始接入TUICallEngine请优先使用join。
*/
- (void)joinInGroupCall:(TUIRoomId *)roomId
groupId:(NSString *)groupId
callMediaType:(TUICallMediaType)callMediaType
succ:(TUICallSucc)succ
fail:(TUICallFail)fail NS_SWIFT_NAME(joinInGroupCall(roomId:groupId:callMediaType:succ:fail:)) __attribute__((deprecated));
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,210 @@
/**
* Copyright (c) 2022 Tencent. All rights reserved.
* Module: TUICallObserver
* Function: TUICallObserver 主功能接口
* Version: <:Version:>
*/
#import <Foundation/Foundation.h>
#import "TUICallDefine.h"
@class TUIRoomId, TUINetworkQualityInfo;
NS_ASSUME_NONNULL_BEGIN
@protocol TUICallObserver <NSObject>
@optional
/**
* 通话过程中错误回调
*
* @param code 错误码
* @param message 错误信息
*/
- (void)onError:(int)code message:(NSString *_Nullable)message NS_SWIFT_NAME(onError(code:message:));
/**
* 收到通话请求的回调(仅被叫收到)
*
* @param callId 此次通话的唯一 ID
* @param callerId 主叫 ID邀请方
* @param calleeIdList 被叫 ID 列表(被邀请方)
* @param mediaType 通话的媒体类型,比如视频通话、语音通话
* @param info 扩展信息
*/
- (void)onCallReceived:(NSString *)callId
callerId:(NSString *)callerId
calleeIdList:(NSArray<NSString *> *)calleeIdList
mediaType:(TUICallMediaType)mediaType
info:(TUICallObserverExtraInfo *)info NS_SWIFT_NAME(onCallReceived(callerId:calleeIdList:mediaType:info:));
/**
* 通话接通的回调(主叫和被叫都可以收到)
*
* @param callId 此次通话的唯一 ID
* @param mediaType 通话的媒体类型,比如视频通话、语音通话
* @param info 扩展信息
*/
- (void)onCallBegin:(NSString *)callId mediaType:(TUICallMediaType)mediaType info:(TUICallObserverExtraInfo *)info NS_SWIFT_NAME(onCallBegin(callId:mediaType:info:));
/**
* 通话结束的回调(主叫和被叫都可以收到)
*
* @param callId 此次通话的唯一 ID
* @param mediaType 通话的媒体类型,比如视频通话、语音通话
* @param reason 通话结束原因
* @param userId 结束通话的用户 ID
* @param totalTime 此次通话的时长
* @param info 扩展信息
*/
- (void)onCallEnd:(NSString *)callId
mediaType:(TUICallMediaType)mediaType
reason:(TUICallEndReason)reason
userId:(NSString *)userId
totalTime:(float)totalTime
info:(TUICallObserverExtraInfo *)info NS_SWIFT_NAME(onCallEnd(callId:mediaType:reason:userId:totalTime:info:));
/**
* 通话取消的回调
*
* @param callId 此次通话的唯一 ID
* @param mediaType 通话的媒体类型,比如视频通话、语音通话
* @param reason 通话结束原因
* @param userId 结束通话的用户 ID
* @param info 扩展信息
*/
- (void)onCallNotConnected:(NSString *)callId
mediaType:(TUICallMediaType)mediaType
reason:(TUICallEndReason)reason
userId:(NSString *)userId
info:(TUICallObserverExtraInfo *)info NS_SWIFT_NAME(onCallNotConnected(callId:mediaType:reason:userId:info:));
/**
* 通话媒体类型发生改变的回调
*
* @param oldCallMediaType 旧的通话类型
* @param newCallMediaType 新的通话类型
*/
- (void)onCallMediaTypeChanged:(TUICallMediaType)oldCallMediaType newCallMediaType:(TUICallMediaType)newCallMediaType NS_SWIFT_NAME(onCallMediaTypeChanged(oldCallMediaType:newCallMediaType:));
/**
* xxxx 用户拒绝通话的回调
*
* @param userId 拒绝用户的 ID
*/
- (void)onUserReject:(NSString *)userId NS_SWIFT_NAME(onUserReject(userId:));
/**
* xxxx 用户不响应的回调
*
* @param userId 无响应用户的 ID
*/
- (void)onUserNoResponse:(NSString *)userId NS_SWIFT_NAME(onUserNoResponse(userId:));
/**
* xxxx 用户忙线的回调
*
* @param userId 忙线用户的 ID
*/
- (void)onUserLineBusy:(NSString *)userId NS_SWIFT_NAME(onUserLineBusy(userId:));
/**
* 当用户被邀请加入通话时的回调
*
* @param userId 被邀请的用户 ID
*/
- (void)onUserInviting:(NSString *)userId NS_SWIFT_NAME(onUserInviting(userId:));
/**
* xxxx 用户加入通话的回调
*
* @param userId 加入当前通话的用户 ID
*/
- (void)onUserJoin:(NSString *)userId NS_SWIFT_NAME(onUserJoin(userId:));
/**
* xxxx 用户离开通话的回调
*
* @param userId 离开当前通话的用户 ID
*/
- (void)onUserLeave:(NSString *)userId NS_SWIFT_NAME(onUserLeave(userId:));
/**
* xxxx 远端用户是否有视频流的回调
*
* @param userId 通话用户 ID
* @param isVideoAvailable 用户视频是否可用
*/
- (void)onUserVideoAvailable:(NSString *)userId isVideoAvailable:(BOOL)isVideoAvailable NS_SWIFT_NAME(onUserVideoAvailable(userId:isVideoAvailable:));
/**
* xxxx 远端用户是否有音频流的回调
*
* @param userId 通话用户 ID
* @param isAudioAvailable 用户音频是否可用
*/
- (void)onUserAudioAvailable:(NSString *)userId isAudioAvailable:(BOOL)isAudioAvailable NS_SWIFT_NAME(onUserAudioAvailable(userId:isAudioAvailable:));
/**
* 所有用户音量大小的反馈回调
*
* @param volumeMap 音量表,根据每个 userId 可以获取对应用户的音量大小音量范围0-100
*/
- (void)onUserVoiceVolumeChanged:(NSDictionary<NSString *, NSNumber *> *)volumeMap NS_SWIFT_NAME(onUserVoiceVolumeChanged(volumeMap:));
/**
* 所有用户网络质量的反馈回调
*
* @param networkQualityList 网络状态,根据每个 userId 可以获取对应用户当前的网络质量
*/
- (void)onUserNetworkQualityChanged:(NSArray<TUINetworkQualityInfo *> *)networkQualityList NS_SWIFT_NAME(onUserNetworkQualityChanged(networkQualityList:));
/**
* 当前用户被踢下线:此时可以 UI 提示用户,并再次重新调用初始化
*/
- (void)onKickedOffline NS_SWIFT_NAME(onKickedOffline());
/**
* 在线时票据过期:此时您需要生成新的 userSig,并再次重新调用初始化
*/
- (void)onUserSigExpired NS_SWIFT_NAME(onUserSigExpired());
/**
* 此接口已于2025年3月废弃推荐使用新的 onCallReceived 回调,功能更强,信息更全面;
*/
- (void)onCallReceived:(NSString *)callerId
calleeIdList:(NSArray<NSString *> *)calleeIdList
groupId:(NSString *_Nullable)groupId
callMediaType:(TUICallMediaType)callMediaType NS_SWIFT_NAME(onCallReceived(callerId:calleeIdList:groupId:callMediaType:)) __attribute__((deprecated("Deprecated from v3.0")));
/**
* 此接口已于2025年3月废弃推荐使用新的 onCallReceived 回调,功能更强,信息更全面;
*/
- (void)onCallReceived:(NSString *)callerId
calleeIdList:(NSArray<NSString *> *)calleeIdList
groupId:(NSString *_Nullable)groupId
callMediaType:(TUICallMediaType)callMediaType
userData:(NSString *_Nullable)userData NS_SWIFT_NAME(onCallReceived(callerId:calleeIdList:groupId:callMediaType:userData:)) __attribute__((deprecated("Deprecated from v3.0")));
/**
* 此接口已于2025年3月废弃, 推荐使用 onCallNotConnected 回调,功能更强,信息更全面;
*/
- (void)onCallCancelled:(NSString *)callerId NS_SWIFT_NAME(onCallCancelled(callerId:)) __attribute__((deprecated("Deprecated from v3.0")));
/**
* 此接口已于2025年3月废弃推荐使用新的 onCallBegin 回调,功能更强,信息更全面;
*/
- (void)onCallBegin:(TUIRoomId *)roomId callMediaType:(TUICallMediaType)callMediaType callRole:(TUICallRole)callRole NS_SWIFT_NAME(onCallBegin(roomId:callMediaType:callRole:)) __attribute__((deprecated("Deprecated from v3.0")));
/**
* 此接口已于2025年3月废弃推荐使用新的 onCallEnd 回调,功能更强,信息更全面;
*/
- (void)onCallEnd:(TUIRoomId *)roomId
callMediaType:(TUICallMediaType)callMediaType
callRole:(TUICallRole)callRole
totalTime:(float)totalTime NS_SWIFT_NAME(onCallEnd(roomId:callMediaType:callRole:totalTime:)) __attribute__((deprecated("Deprecated from v3.0")));
@end
NS_ASSUME_NONNULL_END

Some files were not shown because too many files have changed in this diff Show More