// pages/video/video.ts import UrlParamsHandler from "../../../utils/store"; import { htmlToWxNodes } from "../../../utils/html-to-wx-nodes"; import { request } from "../../../utils/request"; import { AES_IV, AES_KEY, appId } from "../../../env"; import { receiveRedPacket } from "../../../utils/util"; import { aesEncrypt } from "../../../utils/crypto"; const isMobileEnvironment = (): boolean => { const platform = wx.getSystemInfoSync().platform?.toLowerCase() || ''; return platform !== 'windows' && platform !== 'mac'; }; Page({ _wasPlayingBeforeInterrupt: false, _pauseVideoHandler: null as any, _resumeVideoHandler: null as any, _appHideHandler: null as any, _appShowHandler: null as any, _networkStatusChangeHandler: null as any, _videoContext: null as any, /** * 页面的初始数据 */ data: { dataList: {}, nodes: [] as any[], eveId: null, store: {}, fileid: "", active: 0, canSubmit: false, safeBottom: 24, resumeTime: 0, playing: false, currentTime: 0, sendDurationTimer: 0, isAppHidden: false, }, /** * 生命周期函数--监听页面加载 */ async onLoad(options: any) { const app = getApp(); wx.hideShareMenu({ menus: ["shareAppMessage"] }); new UrlParamsHandler(options); app.globalData.store = options; let safeBottom = 24; try { //@ts-ignore const systemInfo = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); safeBottom = Math.abs(systemInfo.windowHeight - systemInfo.safeArea.bottom) + 10; if (safeBottom === 0) safeBottom = 24; } catch (e) { } this.setData({ safeBottom, store: options, resumeTime: this.getResumeTime(), }); this.initSystemListeners(); if (!isMobileEnvironment()) { this.handleEnterError("当前环境不支持观看,请使用手机观看。", "环境不支持"); return; } const bool = await app.login("video"); if (!bool) return; //@ts-ignore 开启防截屏 if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'hidden' }); }, onShow() { this.search(); }, onHide() { //@ts-ignore if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'none' }); if (this.data.currentTime > 0) { wx.setStorageSync("interruptedTime", this.data.currentTime); } }, onUnload() { //@ts-ignore if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'none' }); if (this.data.currentTime > 0) { wx.setStorageSync("interruptedTime", this.data.currentTime); wx.setStorageSync("currentTime", this.data.currentTime); } try { this.pauseVideoBeforeNavigate(); } catch (err) { } // 卸载时清理所有系统级监听与定时器 if (this._pauseVideoHandler) wx.offAudioInterruptionBegin(this._pauseVideoHandler); if (this._resumeVideoHandler) wx.offAudioInterruptionEnd(this._resumeVideoHandler); if (this._appHideHandler) wx.offAppHide(this._appHideHandler); if (this._appShowHandler) wx.offAppShow(this._appShowHandler); if (this._networkStatusChangeHandler) wx.offNetworkStatusChange(this._networkStatusChangeHandler); this.clearSendProgressTimer(); }, onChange(event: any) { this.setData({ active: event.detail.index }); }, initSystemListeners() { this._networkStatusChangeHandler = (res: any) => { if (!res.isConnected) { wx.showToast({ title: '网络连接断开', icon: 'none' }); this.pauseVideoBeforeNavigate(); } else if (res.networkType !== 'wifi') { wx.showToast({ title: '当前为非Wi-Fi环境,请注意流量消耗', icon: 'none' }); } }; wx.onNetworkStatusChange(this._networkStatusChangeHandler); this._pauseVideoHandler = () => { this._wasPlayingBeforeInterrupt = this.data.playing; this.pauseVideoBeforeNavigate(); }; this._resumeVideoHandler = () => { const wasPlaying = this._wasPlayingBeforeInterrupt; this._wasPlayingBeforeInterrupt = false; if (wasPlaying && !this.data.isAppHidden) { this.resumeVideoAfterInterrupt(); } }; this._appHideHandler = () => { this.setData({ isAppHidden: true }); this.pauseVideoBeforeNavigate(); }; this._appShowHandler = () => { this.setData({ isAppHidden: false }); }; wx.onAudioInterruptionBegin(this._pauseVideoHandler); wx.onAudioInterruptionEnd(this._resumeVideoHandler); wx.onAppHide(this._appHideHandler); wx.onAppShow(this._appShowHandler); }, getVideoContext() { if (!this._videoContext) { this._videoContext = wx.createVideoContext("myPlayerId", this); } return this._videoContext; }, pauseVideoBeforeNavigate() { this.getVideoContext().pause(); }, resumeVideoAfterInterrupt() { this.getVideoContext().play(); }, syncResumeTime() { if (this.data.resumeTime > 0) { this.getVideoContext().seek(this.data.resumeTime); } }, onPlayerPlay() { this.setData({ playing: true, resumeTime: 0 }); wx.removeStorageSync("interruptedTime"); this.startSendProgress(); }, onPlayerPause() { this.setData({ playing: false }); this.clearSendProgressTimer(); // 用户主动点击暂停时,存入进度 if (this.data.currentTime > 0) { wx.setStorageSync("interruptedTime", this.data.currentTime); } }, onPlayerEnded() { this.setData({ playing: false, resumeTime: 0 }); wx.setStorageSync("ended", true); this.clearSendProgressTimer(); wx.removeStorageSync("interruptedTime"); wx.removeStorageSync("currentTime"); }, onPlayerError(e: any) { const detail = (e && e.detail) || e || {}; wx.showModal({ title: "播放出错", content: String(detail.errMsg || detail.message || JSON.stringify(detail) + '请联系坐席'), showCancel: false, }); }, // 接收原生组件的时间更新 onPlayerTimeUpdate(e: any) { // 兼容 VOD 插件可能多包一层 detail 的情况 let time = e.detail?.currentTime ?? e.detail?.detail?.currentTime; if (typeof time === 'number' && time > 0) { const currentTime = Math.floor(time); this.data.currentTime = currentTime; // 纯内存更新,不触发无用渲染 // 每 5 秒落盘一次 currentTime 作为终极兜底,不要太频繁避免卡顿 if (currentTime % 5 === 0) { wx.setStorageSync("currentTime", currentTime); } } }, getResumeTime(): number { if (wx.getStorageSync("ended")) return 0; const interrupted = Number(wx.getStorageSync("interruptedTime") || 0); const current = Number(wx.getStorageSync("currentTime") || 0); return Math.max(interrupted, current); }, startSendProgress() { this.clearSendProgressTimer(); this.sendProgress(); const timer = setInterval(() => { this.sendProgress(); }, 15000) as unknown as number; this.setData({ sendDurationTimer: timer }); }, clearSendProgressTimer() { if (this.data.sendDurationTimer) { clearInterval(this.data.sendDurationTimer); this.setData({ sendDurationTimer: 0 }); } }, async sendProgress() { if (!this.data.playing) return; try { const openId = wx.getStorageSync("openId"); const code = wx.getStorageSync("code"); const state = wx.getStorageSync("state"); const seeId = wx.getStorageSync("seeId"); // 视频时长兜底补偿 const duration = this.data.currentTime + 1; const url = code ? `/app/video-watch/inspect` : `/app/watch/inspect`; const params = code ? { openId, code, duration } : { openId, state, seeId, duration }; await request({ options: { url, data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV), }, isLoading: false, }); } catch (err) { console.error("上报观看进度失败:", err); } }, onAnswerChange() { this.updateProgress(); }, async search() { const app = getApp(); if (!(await app.checkLoginState())) return; const code = wx.getStorageSync("code"); const params = { openId: wx.getStorageSync("openId"), ...app.globalData.store }; const url = code ? `/app/video-watch/details` : `/app/watch/details`; const data = await request({ options: { url, method: code ? "GET" : "POST", data: params }, }); const targetData = code ? data.watch : data.data; if (!targetData.extend?.fileid) { const stateParam = code ? -1 : 5; wx.reLaunch({ url: `/pages/live/registration/registration?content=${targetData?.content || ''}&state=${stateParam}`, }); return; } this.setData({ dataList: targetData, eveId: targetData.eveId, nodes: htmlToWxNodes(targetData.content), fileid: targetData.extend.fileid, }); this.syncResumeTime(); this.selectComponent("#tabs")?.resize(); }, updateProgress() { const quizComponent = this.selectComponent("#quizComponent"); if (quizComponent) { this.setData({ canSubmit: quizComponent.isAllAnswered() }); } }, handleSubmit() { if (!wx.getStorageSync("ended")) { return wx.showModal({ title: "请先观看完整视频,再答题", showCancel: false, confirmColor: "#FF0000" }); } //@ts-ignore if (this.data.dataList?.userAnsNum >= this.data.dataList?.answerMax) { return wx.showToast({ title: "答题次数已用完", icon: "none", duration: 3000 }); } const quizComponent = this.selectComponent("#quizComponent"); if (!quizComponent) return wx.showToast({ title: "组件加载失败", icon: "none" }); if (!quizComponent.isAllAnswered()) { const progress = quizComponent.getProgress(); return wx.showModal({ title: "答题未完成", content: `您已完成 ${progress.answered}/${progress.total} 题,请完成所有题目后再提交。`, showCancel: false, }); } const answers = quizComponent.getAllAnswers(); if (!answers?.length) return wx.showToast({ title: "请先完成答题", icon: "none" }); wx.showModal({ title: "确认提交", content: `您已完成 ${answers.length} 道题目,确定要提交答案吗?`, success: (res) => { if (res.confirm) this.submitAnswers(answers); }, }); }, async submitAnswers(answers: any[]) { const code = wx.getStorageSync("code"); const params = { answers, openId: wx.getStorageSync("openId"), eveId: this.data.eveId, ...this.data.store, }; wx.showLoading({ title: "提交中..." }); const url = code ? "/app/video-watch/interactive" : "/app/watch/interactive"; const res = await request({ options: { url, data: params } }); wx.hideLoading(); if (res.code !== 200) { return wx.showToast({ title: res.msg || "提交失败", icon: "none", duration: 2000 }); } const { pass, redPackage } = res.data || {}; if (pass) { wx.showModal({ content: "🎉 回答正确,领取红包!", showCancel: false, confirmText: "立即领取", confirmColor: "#FF0000", success: async (res) => { if (res.confirm) { if (redPackage?.packageInfo) { this.transferRedPacket(redPackage); } else if (redPackage?.redId) { await this.handleReceive(redPackage.redId); } } }, }); } else { wx.showToast({ title: "回答错误,与红包擦肩而过", icon: "none", duration: 3000 }); } }, async transferRedPacket(redPackage: any) { wx.showLoading({ title: "领取中", mask: true }); if (!wx.canIUse?.("requestMerchantTransfer")) { wx.hideLoading(); return wx.showModal({ content: "你的微信版本过低,请更新至最新版本。", showCancel: false }); } try { await new Promise((resolve, reject) => { // @ts-ignore wx.requestMerchantTransfer({ mchId: redPackage.mchId, appId, package: redPackage.packageInfo, success: () => { wx.hideLoading(); wx.showToast({ title: "领取成功", icon: "success" }); resolve(); }, fail: (err: any) => reject(new Error(err?.errMsg || "领取失败")), }); }); } catch (err: any) { wx.hideLoading(); wx.showToast({ title: err?.message || "领取失败", icon: "none" }); } }, async handleReceive(id: any) { const { code } = this.data.store as Record; try { wx.showLoading({ title: "领取中" }); await receiveRedPacket( { appId, code, openId: wx.getStorageSync("openId"), redId: id }, '/app/general/receive-red', appId ); wx.hideLoading(); } catch (err) { wx.hideLoading(); console.error('领取红包失败:', err); } }, handleEnterError(message: string, title = "无法进入") { wx.showModal({ title, content: message, showCancel: false }); }, handleBack() { wx.switchTab({ url: "/pages/tab-bar/course/course" }); }, handleCustomReport() { this.pauseVideoBeforeNavigate(); wx.navigateTo({ url: `/pages/live/report/report` }); }, handleViewHitory() { this.pauseVideoBeforeNavigate(); wx.navigateTo({ url: `/pages/live/red-history/red-history` }); } });