[产品介绍]Flex Live chat

Flex聊天工具LiveChat演示
本文介绍了一款名为LiveChat的聊天工具,该工具由Flex、JQuery和swfobject等技术实现。LiveChat具备多语言支持、自适应语言环境等功能,并能获取用户IP地址及地理位置信息。
今天给大家show一个Flex Project Platform的功能之一:Live Chat(聊天工具)
 
具体的功能如下:
客户端:
1、聊天的客户端类似于新浪Woocall的一个由Flex + JQuery + swfobject完成的Flex程序。
2、可以随意改变大小。
3、可以随意拖拽。
4、embed方式简单,使用一段JS代码放置到任何网页即可完成客户端的安装。
5、支持多语言(中文、英文)
6、自识别当前的语言环境。
 
客服端:
1、可以随意改变大小。
2、可以得到客服端的IP地址。
3、通过IP地址得到相应的国家、城市、经纬度。
4、通过IP可以使用Google map直接定位到当前客户端所在的城市。
5、可以根据用户端所处的语言进行更换相应语言。

老规矩,上视频,无使用地址与source,欢迎大家讨论:)
如果看不清,请看这里: http://rxna.cn/projectvideo/livechat.swf
....

 

转载于:https://www.cnblogs.com/kenshin/archive/2009/03/13/1455359.html

<template> <view class="container"> <!-- 视频播放区 --> <view class="video-container"> <video ref="videoPlayer" :muted="liveData.muted" :autoplay="liveData.autoplay" playsinline webkit-playsinline x5-playsinline object-fit="contain" :poster="liveData.poster" class="video" v-show="!hasError" @error="handleVideoError" @play="onPlay" @pause="onPause" @waiting="onBuffering" @touchstart="handleVideoTouch" ></video> <!-- 状态提示 --> <view v-if="isLoadingComponent" class="loading-mask"> <view class="spinner"></view> <text class="loading-text">视频组件加载中...</text> </view> <view v-if="isLoadingPlayer && !hasError" class="loading-mask"> <view class="spinner"></view> <text class="loading-text">直播加载中...{{ loadProgress }}%</text> </view> <view v-if="hasError" class="error-mask"> <text class="error-title">{{ errorTitle }}</text> <text class="error-desc">{{ errorDesc }}</text> <button class="retry-btn" @click="reloadPlayer">重试</button> <button class="switch-btn" @click="switchSource" v-if="!isSingleSource">切换源</button> </view> <!-- 左下角透明聊天功能 --> <view class="chat-container" :class="{ 'chat-expanded': isChatExpanded }"> <view class="chat-header" @click="toggleChatExpand"> <text class="chat-title">直播聊天</text> <text class="chat-toggle-icon">{{ isChatExpanded ? '−' : '+' }}</text> </view> <view class="chat-messages" v-if="isChatExpanded"> <view class="chat-message" v-for="(msg, idx) in chatMessages" :key="idx"> <image class="chat-avatar" :src="msg.avatar || msg.userAvatar" mode="widthFix" ></image> <view class="chat-content-wrap"> <text class="chat-username">{{ msg.username||msg.userNickname }}:</text> <text class="chat-content" :class="{ 'system-msg': msg.type !== 'chat' }"> {{ msg.content }} </text> </view> </view> </view> <view class="chat-input-area"> <input class="chat-input" v-model="chatInput" placeholder="输入消息..." placeholder-style="color: rgba(255,255,255,0.5)" @confirm="sendChatMessage" :disabled="!isChatExpanded" /> <button class="chat-send-btn" @click="sendChatMessage" :disabled="!isChatExpanded || !chatInput.trim()" > <!-- || wsStatus !== 'open' 需要在disabled里面添加这个实时通信 --> 发送 </button> </view> </view> </view> </view> </template> <script> import Hls from 'hls.js'; import { livecomment,liveroomlike } from '@/api/graphic.js'; export default { data() { return { queryliat:{//直播间评论 roomId:80, userId:null, content:null, }, statusBarHeight: 0, isUsingCustomSource: true, isSingleSource: false, sources: { test: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', custom: 'http://139.159.156.31:8082/live/test_stream.m3u8', appBackup: 'https://backup-cdn.com/live/app_stream.m3u8' }, liveData: { muted: false, autoplay: true, poster: 'https://picsum.photos/800/450' }, isLoadingComponent: true, isLoadingPlayer: false, isBuffering: false, hasError: false, errorTitle: '', errorDesc: '', loadProgress: 0, initRetryCount: 0, maxInitRetry: 3, hls: null, isNativeHLS: false, isPlaying: false, // 聊天功能相关 isChatExpanded: false, chatInput: '', chatMessages: [ { username: '系统', content: '欢迎进入直播聊天室~', type: 'system', avatar: '' } ], // 实时通信相关 ws: null, wsUrl: 'ws://192.168.1.4:8081/ws/live/comment', // 替换为实际WebSocket服务地址 wsStatus: 'closed', // closed/connecting/open/error reconnectCount: 0, maxReconnect: 5, roomId: '', userInfo: { username: uni.getStorageSync('username') || '观众' + Math.floor(Math.random() * 1000), avatar: uni.getStorageSync('avatar') || 'https://picsum.photos/100/100?random=' + Math.floor(Math.random() * 100) } }; }, computed: { currentSource() { return this.isUsingCustomSource ? this.sources.custom : this.sources.test; } }, onLoad(options) { console.log('接收的直播参数:', options); // 初始化直播间ID(用于隔离聊天房间) this.roomId = options.roomId || options.id || 'defaultLiveRoom'; if (options.id) this.sources.custom = options.id; const systemInfo = uni.getSystemInfoSync(); this.statusBarHeight = systemInfo.statusBarHeight || 0; this.checkCustomSourceAvailability(); setTimeout(() => { this.$nextTick(() => { this.initVideoComponent(); this.initWebSocket(); // 初始化实时聊天 }); }, 500); }, onUnload() { this.destroyPlayer(); // 关闭WebSocket连接 if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.sendWsMessage({ type: 'leave', content: '离开了直播间' }); setTimeout(() => { this.ws.close(1000, '用户离开'); }, 500); } }, methods: { checkCustomSourceAvailability() { // 移除XMLHttpRequest相关代码,改用uni.request uni.request({ url: this.sources.custom, method: 'HEAD', // 发送HEAD请求检测源是否可用 timeout: 3000, // 超时时间3秒 success: (res) => { // 状态码200-399表示源可访问 if (res.statusCode >= 200 && res.statusCode < 400) { console.log('自定义源可访问'); } else { console.warn('自定义源不可访问,切换到测试源'); this.isUsingCustomSource = false; this.isSingleSource = false; } }, fail: (err) => { console.error('自定义源请求失败,切换到测试源', err); this.isUsingCustomSource = false; this.isSingleSource = false; } }); }, initVideoComponent() { const uniVideoRef = this.$refs.videoPlayer; if (!uniVideoRef) { this.retryInit('$refs.videoPlayer 未找到(组件未渲染)'); return; } // 第二步:检查$el是否存在(部分场景下组件可能已卸载) if (!uniVideoRef.$el) { this.retryInit('video组件的$el不存在(组件已卸载)'); return; } // 第三步:安全获取video元素 const videoEl = uniVideoRef.$el.querySelector('video'); if (!videoEl || videoEl.tagName !== 'VIDEO') { this.retryInit('未提取到原生video标签'); return; } this.isLoadingComponent = false; this.checkNativeHLSSupport(videoEl); this.initPlayer(videoEl); }, retryInit(reason) { console.warn(`初始化重试(${this.initRetryCount + 1}/${this.maxInitRetry}):${reason}`); if (this.initRetryCount < this.maxInitRetry) { this.initRetryCount++; setTimeout(() => this.initVideoComponent(), 1000); return; } this.isLoadingComponent = false; this.setError('组件初始化失败', reason + ',请重启项目或检查设备'); }, // 修改checkNativeHLSSupport方法,增加更严格的格式检测 checkNativeHLSSupport(videoEl) { try { // 增加HLS格式白名单检测 const hlsTypes = ['application/vnd.apple.mpegurl', 'application/x-mpegURL']; const supportLevel = hlsTypes.map(type => videoEl.canPlayType(type)) .find(level => level === 'probably' || level === 'maybe'); this.isNativeHLS = !!supportLevel; console.log('严格HLS支持检测:', this.isNativeHLS); } catch (err) { this.isNativeHLS = false; console.error('HLS支持检测异常:', err); } }, initPlayer(videoEl) { this.isLoadingPlayer = true; // App端特殊处理 if (uni.getSystemInfoSync().platform !== 'h5') { // 先静音播放(绕过自动播放限制) videoEl.muted = true; this.liveData.muted = true; // 增加用户交互后恢复声音 const restoreAudio = () => { videoEl.muted = false; this.liveData.muted = false; document.removeEventListener('click', restoreAudio); }; document.addEventListener('click', restoreAudio, { once: true }); } if (this.isNativeHLS) { this.playWithNative(videoEl); return; } this.playWithHLS(videoEl); }, playWithNative(videoEl) { videoEl.src = this.currentSource; videoEl.addEventListener('progress', () => { if (videoEl.buffered.length > 0) { const loadedEnd = videoEl.buffered.end(videoEl.buffered.length - 1); this.loadProgress = Math.floor((loadedEnd / (videoEl.duration || 1)) * 100) || 0; } }); videoEl.play() .then(() => { this.isLoadingPlayer = false; this.isPlaying = true; }) .catch(err => this.setError('原生播放失败', err.message || '自动播放被拦截')); }, playWithHLS(videoEl) { if (uni.getSystemInfoSync().platform === 'ios' || uni.getSystemInfoSync().platform === 'android') { // App端强制使用原生播放(如果可能) if (this.isNativeHLS) { this.playWithNative(videoEl); return; } // 增加App端HLS.js配置 this.hls = new Hls({ // App端需要关闭的配置项 enableWorker: false, lowLatencyMode: true, // 增加App端专用配置 appSpecific: { useNativeHLSWhenAvailable: true } }); } if (typeof Hls === 'undefined') { this.setError('依赖缺失', '请安装HLS.js(npm install hls.js)'); this.isLoadingPlayer = false; return; } if (this.hls) { this.hls.destroy(); this.hls = null; } this.hls = new Hls({ maxBufferLength: 20, maxMaxBufferLength: 30, startLevel: 0, enableWorker: false, lowLatencyMode: false, fragLoadTimeOut: 30000, fragMaxRetry: 3 }); this.hls.attachMedia(videoEl); this.hls.on(Hls.Events.ERROR, (_, data) => { console.error('HLS.js错误:', data); if (data.fatal) { const errMap = { networkError: '网络错误(检查网络或源地址)', mediaError: '格式不兼容(需H.264+AAC编码)', manifestError: 'm3u8清单格式错误' }; this.setError(errMap[data.type] || 'HLS播放失败', data.details || '未知错误'); this.isSingleSource = Object.keys(this.sources).length <= 1; this.isLoadingPlayer = false; } }); this.hls.loadSource(this.currentSource); this.hls.on(Hls.Events.MANIFEST_PARSED, () => { console.log('HLS清单解析成功'); this.isLoadingPlayer = false; videoEl.play().then(() => { this.isPlaying = true; }).catch(err => { this.setError('播放失败', err.name === 'NotAllowedError' ? '自动播放被拦截(点击视频开启)' : err.message); }); }); this.hls.on(Hls.Events.BUFFER_LEVEL_STATE_CHANGED, (_, data) => { if (data.level === -1) return; this.loadProgress = Math.floor((data.bufferLength / (data.targetDuration || 1)) * 100) || 0; }); }, switchSource() { this.isUsingCustomSource = !this.isUsingCustomSource; this.destroyPlayer(); this.$nextTick(() => { this.isLoadingComponent = true; this.initVideoComponent(); }); }, reloadPlayer() { this.hasError = false; this.isLoadingComponent = true; this.initRetryCount = 0; this.destroyPlayer(); this.$nextTick(() => { this.initVideoComponent(); }); }, destroyPlayer() { if (this.hls) { this.hls.destroy(); this.hls = null; } const uniVideoRef = this.$refs.videoPlayer; if (uniVideoRef) { const videoEl = uniVideoRef.$el.querySelector('video'); if (videoEl) { videoEl.pause(); videoEl.src = ''; videoEl.load(); } } this.isLoadingPlayer = false; this.loadProgress = 0; this.isPlaying = false; }, setError(title, desc) { this.hasError = true; this.errorTitle = title; this.errorDesc = desc; this.isLoadingPlayer = false; this.isLoadingComponent = false; }, handleVideoError(err) { const errDetail = err.detail || {}; if (errDetail.errCode || errDetail.errMsg) { console.error('uni-video错误:', errDetail); this.setError('视频组件错误', `错误码: ${errDetail.errCode || '未知'}, 信息: ${errDetail.errMsg || '无'}`); } }, toggleMute() { this.liveData.muted = !this.liveData.muted; const uniVideoRef = this.$refs.videoPlayer; if (uniVideoRef) { const videoEl = uniVideoRef.$el.querySelector('video'); if (videoEl) videoEl.muted = this.liveData.muted; } }, onPlay() { this.isBuffering = false; this.isPlaying = true; }, onPause() { if (!this.hasError) this.isBuffering = true; this.isPlaying = false; }, onBuffering() { this.isBuffering = true; }, // 聊天功能方法 toggleChatExpand() { this.isChatExpanded = !this.isChatExpanded; this.$nextTick(() => this.scrollToChatBottom()); }, sendChatMessage() { const userlist = uni.getStorageSync('UID'); const datalist = uni.getStorageSync('USER_INFO'); const userInfo = JSON.parse(datalist); this.queryliat.userId=userlist const content = this.chatInput.trim(); this.queryliat.content=content if (!content) return; this.chatInput = ''; this.scrollToChatBottom(); livecomment(80,userlist,content).then(res => { console.log(res,'asdas') }); // 发送到WebSocket后端 this.sendWsMessage({ content: content }); }, scrollToChatBottom() { if (!this.isChatExpanded) return; const msgContainer = uni.createSelectorQuery().in(this).select('.chat-messages'); msgContainer.fields({ scrollOffset: true, size: true }, res => { if (res) { msgContainer.scrollOffset({ scrollTop: res.scrollHeight, duration: 300 }); } }).exec(); }, // 实时通信方法 initWebSocket() { // 避免重复连接 if (this.wsStatus === 'connecting' || this.wsStatus === 'open') return; // 1. 验证WebSocket URL格式(防止URL错误) if (!this.wsUrl.startsWith('ws://') && !this.wsUrl.startsWith('wss://')) { console.error('WebSocket URL格式错误,必须以ws://或wss://开头'); this.showToast('聊天服务地址错误'); return; } // 拼接完整URL(固定roomId为80,确保参数正确) const wsUrlWithRoom = `${this.wsUrl}/80`; console.log('尝试连接WebSocket:', wsUrlWithRoom); this.wsStatus = 'connecting'; // 2. 增加连接超时处理(防止一直卡在connecting状态) const connectionTimeout = setTimeout(() => { if (this.wsStatus === 'connecting') { console.error('WebSocket连接超时(10秒未响应)'); this.wsStatus = 'error'; this.showToast('聊天连接超时'); this.destroyWebSocket(); // 主动销毁超时连接 this.reconnectWebSocket(); } }, 10000); // 10秒超时 try { this.ws = new WebSocket(wsUrlWithRoom); // 连接成功 this.ws.onopen = () => { clearTimeout(connectionTimeout); // 清除超时器 console.log('WebSocket连接成功'); this.wsStatus = 'open'; this.reconnectCount = 0; // 重置重连计数 this.sendWsMessage({ type: 'enter', content: '进入了直播间' }); }; // 接收消息(保持不变) this.ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.username === this.userInfo.username && msg.type !== 'enter') return; console.log(msg.data,'获取消息') this.chatMessages.push(msg.data); this.$nextTick(() => this.scrollToChatBottom()); } catch (err) { console.error('解析WebSocket消息失败:', err); } }; // 3. 增强错误信息输出(关键修改) this.ws.onerror = (event) => { clearTimeout(connectionTimeout); // 打印完整错误上下文 console.error('WebSocket错误详情:'); console.error('连接URL:', wsUrlWithRoom); console.error('错误事件:', event); console.error('WebSocket状态:', this.ws?.readyState ? ['连接中', '已连接', '关闭中', '已关闭'][this.ws.readyState] : '未初始化'); // 根据状态提示可能的原因 let errorMsg = '聊天连接失败'; if (this.ws?.readyState === 3) { // 已关闭状态 errorMsg += '(服务端拒绝连接或地址错误)'; } else if (this.ws?.readyState === 0) { // 连接中出错 errorMsg += '(网络异常或服务未启动)'; } this.wsStatus = 'error'; this.showToast(errorMsg); this.reconnectWebSocket(); }; // 4. 处理关闭事件(根据关闭代码判断原因) this.ws.onclose = (event) => { clearTimeout(connectionTimeout); console.log(`WebSocket关闭 - 代码: ${event.code}, 原因: ${event.reason || '无'}`); this.wsStatus = 'closed'; // 常见关闭代码含义: // 1000: 正常关闭;1006: 异常关闭(服务端未启动/网络断连);1011: 服务端错误 const shouldReconnect = [1006, 1011, 1012].includes(event.code) && this.reconnectCount < this.maxReconnect; if (shouldReconnect) { this.reconnectWebSocket(); } else if (event.code !== 1000) { this.showToast(`聊天连接已关闭(代码: ${event.code})`); } }; } catch (err) { clearTimeout(connectionTimeout); console.error('创建WebSocket实例失败:', err); this.wsStatus = 'error'; // 捕获URL格式错误等初始化异常 this.showToast('聊天服务初始化失败(可能是地址格式错误)'); this.reconnectWebSocket(); } }, // 新增:主动销毁WebSocket连接的方法 destroyWebSocket() { if (this.ws) { try { this.ws.close(1000, '主动关闭'); } catch (err) { console.error('关闭WebSocket失败:', err); } this.ws = null; } }, // 优化:重连机制(指数退避策略) reconnectWebSocket() { if (this.reconnectCount >= this.maxReconnect) { console.error(`已达最大重连次数(${this.maxReconnect}次),停止重连`); this.showToast('多次连接失败,请检查服务状态'); return; } // 指数退避:1s → 2s → 4s → 8s(最多8秒) const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 8000); this.reconnectCount++; console.log(`第${this.reconnectCount}次重连(延迟${delay}ms)`); setTimeout(() => { this.destroyWebSocket(); // 先销毁旧连接 this.initWebSocket(); }, delay); }, reconnectWebSocket() { if (this.reconnectCount >= this.maxReconnect) { this.showToast('聊天连接失败,请刷新页面'); return; } const delay = (this.reconnectCount + 1) * 2000; setTimeout(() => { this.reconnectCount++; console.log(`第${this.reconnectCount}次重连`); this.initWebSocket(); }, delay); }, sendWsMessage(msgData) { if (this.wsStatus !== 'open' || !this.ws) { this.showToast('聊天连接中,请稍后'); return; } const msg = { username: this.userInfo.username, avatar: this.userInfo.avatar, content: '', type: 'chat', timestamp: new Date().getTime(), ...msgData }; try { this.ws.send(JSON.stringify(msg)); } catch (err) { console.error('发送消息失败:', err); this.showToast('消息发送失败'); } }, showToast(title) { uni.showToast({ title, icon: 'none', duration: 1500 }); } } }; </script> <style scoped> .container { width: 100%; height: 100vh; background-color: #000; display: flex; flex-direction: column; overflow: hidden; } .video-container { flex: 1; position: relative; width: 100%; } /* 隐藏视频控件 */ ::v-deep video::-webkit-media-controls { display: none !important; } ::v-deep video::-moz-media-controls { display: none !important; } ::v-deep video::-ms-media-controls { display: none !important; } ::v-deep video::-o-media-controls { display: none !important; } ::v-deep video::-webkit-media-controls-enclosure { display: none !important; } ::v-deep video::-webkit-media-controls-panel { display: none !important; } .video { width: 100%; height: 100%; pointer-events: none; } /* 状态提示样式 */ .loading-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 5; } .spinner { width: 80rpx; height: 80rpx; border: 8rpx solid rgba(255, 255, 255, 0.2); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; } .loading-text { color: #fff; font-size: 28rpx; margin-top: 20rpx; } .error-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40rpx; gap: 20rpx; z-index: 5; } .error-title { color: #ff4d4f; font-size: 32rpx; text-align: center; } .error-desc { color: #fff; font-size: 26rpx; text-align: center; line-height: 1.5; } .retry-btn, .switch-btn { background-color: #1677ff; color: #fff; border-radius: 30rpx; padding: 15rpx 60rpx; font-size: 28rpx; width: auto; margin-top: 10rpx; } .switch-btn { background-color: #4CAF50; } /* 聊天功能样式 */ .chat-container { position: absolute; bottom: 0rpx; left: 5rpx; width: calc(100% - 20rpx); background-color: rgba(0, 0, 0, 0.5); border-radius: 20rpx; overflow: hidden; z-index: 4; transition: height 0.3s ease; } .chat-header { display: flex; justify-content: space-between; align-items: center; padding: 15rpx 20rpx; background-color: rgba(0, 0, 0, 0.6); } .chat-title { color: #fff; font-size: 28rpx; font-weight: 500; } .chat-toggle-icon { color: #fff; font-size: 32rpx; font-weight: bold; } .chat-messages { height: 300rpx; padding: 15rpx 20rpx; overflow-y: auto; display: none; } .chat-expanded .chat-messages { display: block; } .chat-message { display: flex; align-items: flex-start; margin-bottom: 15rpx; line-height: 1.5; } .chat-avatar { width: 40rpx; height: 40rpx; border-radius: 50%; margin-right: 10rpx; margin-top: 5rpx; } .chat-content-wrap { display: flex; flex-direction: column; } .chat-username { color: #40a9ff; font-size: 26rpx; margin-right: 10rpx; font-weight: 500; } .chat-content { color: #fff; font-size: 26rpx; } .system-msg { color: #b3b3b3; font-style: italic; } .chat-input-area { display: flex; align-items: center; padding: 15rpx 20rpx; border-top: 1px solid rgba(255, 255, 255, 0.1); } .chat-input { flex: 1; height: 50rpx; color: #fff; font-size: 26rpx; background-color: rgba(255, 255, 255, 0.1); border-radius: 30rpx; padding: 0 20rpx; margin-right: 15rpx; } .chat-send-btn { background-color: #1677ff; color: #fff; font-size: 26rpx; border-radius: 30rpx; padding: 10rpx 30rpx; } .chat-send-btn:disabled { background-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.6); } /* 动画和滚动条 */ @keyframes spin { to { transform: rotate(360deg); } } .chat-messages::-webkit-scrollbar { width: 8rpx; } .chat-messages::-webkit-scrollbar-track { background-color: rgba(255, 255, 255, 0.1); border-radius: 10rpx; } .chat-messages::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.3); border-radius: 10rpx; } </style>把这个代码兼容app播放
09-10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值