[Human] chore(project): 初始化导入 91dj H5 客户端项目代码
从本地工程首次入库;.npmrc(含私有 npm 源 token)与 tsbuildinfo 已加入 .gitignore,不随仓库提交。
This commit is contained in:
@@ -0,0 +1,847 @@
|
||||
<template>
|
||||
<div class="vertical_video_warp" :data-hide-progress="state.hideProgress">
|
||||
<div class="trial-seek-veil" v-show="trialSeekBlocking"></div>
|
||||
<!-- 点击层必须在 playerShell 外:xgplayer destroy 会 root.innerHTML='',壳内节点会被清掉 -->
|
||||
<div class="avoid_sliding" @click="togglePlayPause"></div>
|
||||
<div :id="playerShellId" class="video-shell"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import VideoApi from '@/service/video';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { onActivated, onBeforeUnmount, onDeactivated, onMounted, Ref, ref, toRefs, reactive } from 'vue';
|
||||
import HlsJSPlayer from 'xgplayer-hls.js';
|
||||
import UserApi from '@/service/user';
|
||||
import { addVideoHistory } from '@/utils';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { analyticsEventVideo } from '@/utils/eventReporter/analytics';
|
||||
import { judgeTime } from '@/utils/index';
|
||||
import {
|
||||
bindXgTrialProgressGuard,
|
||||
setTrialVideoFrameHidden,
|
||||
syncXgplayerSeekUi,
|
||||
XG_HLS_FEED_OPTS,
|
||||
} from '@/utils/xgHlsFeedOpts';
|
||||
import { bindH265PlaybackFallback, resolvePlayResourceKey } from '@/utils/h265Playback';
|
||||
import { setSharedFreeWatchCount } from '@/utils/sharedFreeWatchCount';
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
hasDarkWebTag: boolean;
|
||||
searchAccessTicket?: string;
|
||||
}>();
|
||||
const { videoInfo } = toRefs(props);
|
||||
const playerShellId = `horizontal-hls-${props.videoInfo.id}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
/** 试看拖动/seek 期间黑场遮罩,避免 iOS 先闪出越界帧 */
|
||||
const trialSeekBlocking = ref(false);
|
||||
const { isVip, userInfo } = useUserStore();
|
||||
const userStore = useUserStore();
|
||||
const globalStore = useGlobalStore();
|
||||
let userFreeCount: Ref<Types.Video.UserFreeCount> = ref({
|
||||
isCan: false,
|
||||
watchCount: 0,
|
||||
});
|
||||
const emit = defineEmits(['getFreeCount', 'showPreviewTime', 'play', 'jumpOverHandle', 'timeupdate']);
|
||||
let trialUnlockPopupPending = false;
|
||||
const emitTrialUnlockPopup = () => {
|
||||
if (trialUnlockPopupPending) return;
|
||||
trialUnlockPopupPending = true;
|
||||
emit('jumpOverHandle');
|
||||
window.setTimeout(() => {
|
||||
trialUnlockPopupPending = false;
|
||||
}, 300);
|
||||
};
|
||||
let player: any = null; //播放器实例
|
||||
let isReport = ref(false); // 视频埋点锁
|
||||
let isPreview = ref(false); // 是否预览视频
|
||||
let videoUrl = ref(''); // 播放地址
|
||||
/** 当前是否使用 H.265 资源 */
|
||||
let isPlayingH265 = false;
|
||||
/** H.265 失败后强制 H.264,防止死循环 */
|
||||
let forceH264 = false;
|
||||
let pendingResumeAt = 0;
|
||||
let unbindH265Fallback: (() => void) | null = null;
|
||||
const state = reactive({
|
||||
beginTime: new Date(), // 统计时使用:开始时间
|
||||
playTimer: <any>'', // 统计时使用:时间计时器
|
||||
playTime: 0, // 统计时使用:播放时间
|
||||
hideProgress: false,
|
||||
isCompleteReported: false, // 播放完成事件(>=95%)是否已上报,避免重复
|
||||
isSecondPlayReported: false, // 播放超过 1 分钟后二次 play 埋点锁
|
||||
});
|
||||
|
||||
// 进度达到 95% 视为播放完成(自然播放或拖动到 95% 都算)
|
||||
const checkVideoComplete = () => {
|
||||
if (state.isCompleteReported) return;
|
||||
const duration = Number(player?.video?.duration) || 0;
|
||||
const currentTime = Number(player?.video?.currentTime) || 0;
|
||||
if (duration > 0 && currentTime / duration >= 0.9) {
|
||||
state.isCompleteReported = true;
|
||||
analyticsEventVideo(videoInfo.value, 'MOVIE', Math.ceil(currentTime || 1), 'video_complete', '播放完成');
|
||||
}
|
||||
};
|
||||
|
||||
const checkSecondVideoPlay = () => {
|
||||
if (state.isSecondPlayReported) return;
|
||||
const duration = Number(player?.video?.duration) || 0;
|
||||
const currentTime = Number(player?.video?.currentTime) || 0;
|
||||
if (currentTime / duration < 0.9) {
|
||||
state.isSecondPlayReported = true;
|
||||
analyticsEventVideo(videoInfo.value, 'MOVIE', Math.ceil(currentTime || 1), 'video_play', '播放');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getInfo();
|
||||
//获取免费观看次数
|
||||
getFreeWatchCount();
|
||||
analyticsEventVideo(videoInfo.value, 'MOVIE', 0, 'video_view', '展示');
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 暂停
|
||||
*/
|
||||
const pause = () => {
|
||||
if (!player) return;
|
||||
const wasPlaying = !player.paused;
|
||||
player.pause();
|
||||
clearInterval(state.playTimer);
|
||||
state.playTimer = '';
|
||||
if (wasPlaying) {
|
||||
analyticsEventVideo(videoInfo.value, 'MOVIE', Math.ceil(player?.video?.currentTime || 1), 'video_pause', '暂停');
|
||||
}
|
||||
};
|
||||
|
||||
/** 点击画面切换播放/暂停 */
|
||||
const togglePlayPause = () => {
|
||||
if (!player) return;
|
||||
if (player.paused) {
|
||||
player.play?.();
|
||||
return;
|
||||
}
|
||||
pause();
|
||||
};
|
||||
/**
|
||||
* @description 是否拥有完整片源的观看权限,判定口径与 playCheck 保持一致
|
||||
*/
|
||||
const hasFullVideoPermission = () => {
|
||||
// 已购买
|
||||
if (videoInfo.value.vidStatus.hasPaid === true) return true;
|
||||
// 免费视频
|
||||
if (videoInfo.value.freeArea === true) return true;
|
||||
// 金币视频免费看权限
|
||||
if (videoInfo.value.originCoins != 0 && videoInfo.value.coins == 0) return true;
|
||||
// vip>=2 可直接观看,暗网需 vip4
|
||||
if (isVip && ((userInfo.vipLevel >= 2 && !props.hasDarkWebTag) || (userInfo.vipLevel == 4 && props.hasDarkWebTag))) {
|
||||
return true;
|
||||
}
|
||||
// 会员视频(金币为 0)
|
||||
if (videoInfo.value.originCoins === 0 && isVip) return true;
|
||||
// 自己发布的视频
|
||||
if (userInfo.uid === videoInfo.value.publisher.uid) return true;
|
||||
// 免费观影次数
|
||||
if (userFreeCount.value.isCan === true && videoInfo.value.originCoins === 0) return true;
|
||||
// 预售金币视频免费次数
|
||||
if (
|
||||
useGlobalStore().presaleObj.status != 0 &&
|
||||
globalStore.advanceStatus.privilegeLimit.hasLimit &&
|
||||
useGlobalStore().preSaleCoinVideoPlay
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* @description 判断是否预览视频
|
||||
*/
|
||||
const checkPreview = () => {
|
||||
if (!hasFullVideoPermission() && videoInfo.value.previewURL) {
|
||||
isPreview.value = true;
|
||||
isPlayingH265 = false;
|
||||
videoUrl.value = videoInfo.value.previewURL;
|
||||
} else {
|
||||
isPreview.value = false;
|
||||
const resolved = resolvePlayResourceKey({
|
||||
h264Key: videoInfo.value.sourceURL,
|
||||
h265Key: videoInfo.value.h265Url,
|
||||
forceH264,
|
||||
});
|
||||
isPlayingH265 = resolved.isH265;
|
||||
videoUrl.value = resolved.resourceKey;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param uid
|
||||
*/
|
||||
const getInfo = async () => {
|
||||
try {
|
||||
let res: any = await UserApi.queryUserInfo(null);
|
||||
let userInfo = res;
|
||||
userStore.saveUserInfo(userInfo);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @description 初始化播放器
|
||||
*/
|
||||
const previewStartFlag = ref(false); // 是否进行预览时间设置
|
||||
let suppressPlayAnalytics = false;
|
||||
let trialLimitHandled = false;
|
||||
let unbindTrialProgressGuard: (() => void) | null = null;
|
||||
const pageActive = ref(true);
|
||||
|
||||
const teardownTrialGuard = () => {
|
||||
unbindTrialProgressGuard?.();
|
||||
unbindTrialProgressGuard = null;
|
||||
setTrialFrameBlocked(false);
|
||||
trialLimitHandled = false;
|
||||
};
|
||||
|
||||
let destroySilently = false;
|
||||
|
||||
const destroyPlayer = (silent = false) => {
|
||||
teardownTrialGuard();
|
||||
unbindH265Fallback?.();
|
||||
unbindH265Fallback = null;
|
||||
if (!player) return;
|
||||
if (!silent) checkSecondVideoPlay();
|
||||
destroySilently = silent;
|
||||
try {
|
||||
player.src = '';
|
||||
player.destroy(true);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
destroySilently = false;
|
||||
clearInterval(state.playTimer);
|
||||
state.playTimer = '';
|
||||
player = null;
|
||||
};
|
||||
|
||||
const fallbackH265ToH264 = (resumeAt: number) => {
|
||||
if (forceH264 || !isPlayingH265) return;
|
||||
forceH264 = true;
|
||||
isPlayingH265 = false;
|
||||
pendingResumeAt = resumeAt;
|
||||
destroyPlayer(true);
|
||||
initVideo();
|
||||
};
|
||||
|
||||
const initVideo = () => {
|
||||
if (player) destroyPlayer();
|
||||
checkPreview();
|
||||
player = new HlsJSPlayer({
|
||||
id: playerShellId,
|
||||
url: getPath(),
|
||||
fluid: true,
|
||||
playsinline: true,
|
||||
videoInit: true,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
lang: 'zh-cn',
|
||||
ignores: ['i18n', 'play', 'volume', 'definition'],
|
||||
allowPlayAfterEnded: true,
|
||||
allowSeekAfterEnded: true,
|
||||
closeVideoPreventDefault: true,
|
||||
closeVideoStopPropagation: true,
|
||||
// 由壳外 avoid_sliding 接管单击播停;开启双击会吞掉单击
|
||||
closeVideoClick: true,
|
||||
closeInactive: true,
|
||||
enableVideoDbltouch: false,
|
||||
closeMoveSeek: needsTrialSeekLimit(),
|
||||
hlsOpts: XG_HLS_FEED_OPTS,
|
||||
});
|
||||
unbindH265Fallback = bindH265PlaybackFallback(player, {
|
||||
shouldFallback: () => isPlayingH265 && !forceH264,
|
||||
onFallback: fallbackH265ToH264,
|
||||
});
|
||||
// 播放统计
|
||||
const addPlayCount = () => {
|
||||
//当视频滑走的时候调用 用作统计
|
||||
let req: any = {
|
||||
publisher: videoInfo.value.publisher.uid,
|
||||
videoID: videoInfo.value.id,
|
||||
beginTime: state.beginTime,
|
||||
endTime: new Date(),
|
||||
playWay: userFreeCount.value.isCan ? (userFreeCount.value.watchCount ? 0 : 1) : 2,
|
||||
via: 1,
|
||||
longer: state.playTime,
|
||||
};
|
||||
if (videoInfo.value.tags && videoInfo.value.tags.length > 0) {
|
||||
req.tagID = videoInfo.value.tags[0].id;
|
||||
}
|
||||
if (props.searchAccessTicket) {
|
||||
req.searchAccessTicket = props.searchAccessTicket;
|
||||
}
|
||||
VideoApi.getPlayVideo(req);
|
||||
if (state.playTime > 1) {
|
||||
// analyticsEventVideo(videoInfo.value, 'MOVIE', state.playTime, 'video_exit', '播放退出');
|
||||
}
|
||||
clearInterval(state.playTimer);
|
||||
state.playTimer = '';
|
||||
state.playTime = 1;
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
emit('showPreviewTime', player.video.duration);
|
||||
bindTrialProgressGuard();
|
||||
player.config.closeMoveSeek = needsTrialSeekLimit();
|
||||
}, 500);
|
||||
player.on('playing', () => {
|
||||
playing();
|
||||
});
|
||||
// 设置开始播放时间
|
||||
player.on('loadedmetadata', () => {
|
||||
if (pendingResumeAt > 0) {
|
||||
const resumeAt = pendingResumeAt;
|
||||
pendingResumeAt = 0;
|
||||
try {
|
||||
player.currentTime = resumeAt;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
//免费视频
|
||||
if (videoInfo.value.freeArea === true) return;
|
||||
// 视频已购买
|
||||
if (videoInfo.value.vidStatus.hasPaid === true) return;
|
||||
//金币视频免费看权限
|
||||
if (videoInfo.value.originCoins != 0 && videoInfo.value.coins == 0) return;
|
||||
// 会员视频(金币为0,freeArea = false)
|
||||
if (videoInfo.value.originCoins === 0 && videoInfo.value.freeArea === false && isVip) return;
|
||||
// VIP免费次数
|
||||
if (userFreeCount.value.isCan === true && videoInfo.value.originCoins === 0) return;
|
||||
//自己发布的视频
|
||||
if (userInfo.uid === videoInfo.value.publisher.uid) return;
|
||||
//预售金币视频免费次数能否观看
|
||||
if (
|
||||
useGlobalStore().presaleObj.status != 0 &&
|
||||
globalStore.advanceStatus.privilegeLimit.hasLimit &&
|
||||
useGlobalStore().preSaleCoinVideoPlay
|
||||
)
|
||||
return;
|
||||
player.currentTime = videoInfo.value.previewStart;
|
||||
previewStartFlag.value = true;
|
||||
});
|
||||
// 播放
|
||||
player.on('play', () => {
|
||||
emit('play');
|
||||
if (!suppressPlayAnalytics) {
|
||||
analyticsEventVideo(videoInfo.value, 'MOVIE', Math.ceil(player?.video?.currentTime || 1), 'video_play', '播放');
|
||||
}
|
||||
});
|
||||
|
||||
/** 仅当明确回到试看区内才清标记;回弹到 limit-ε 时 playCheck 为 true,不能清,否则半屏关了又弹 */
|
||||
const canResetTrialLimitHandled = () => {
|
||||
const limit = getBackendTrialLimitSeconds();
|
||||
if (limit == null) return true;
|
||||
const t = Number(player?.video?.currentTime) || 0;
|
||||
return t < limit - Math.max(0.5, TRIAL_SEEK_EPSILON * 2);
|
||||
};
|
||||
|
||||
const handleTrialLimitReached = () => {
|
||||
if (!pageActive.value) return;
|
||||
if (!player.paused) {
|
||||
pause();
|
||||
}
|
||||
if (!trialLimitHandled) {
|
||||
trialLimitHandled = true;
|
||||
revealTrialFrameForMask();
|
||||
emitTrialUnlockPopup();
|
||||
}
|
||||
};
|
||||
|
||||
player.on('timeupdate', () => {
|
||||
if (!pageActive.value || !player.hasStart) return;
|
||||
emit('timeupdate', Number(player?.video?.currentTime) || 0);
|
||||
checkVideoComplete();
|
||||
if (playCheck()) {
|
||||
if (canResetTrialLimitHandled()) {
|
||||
trialLimitHandled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleTrialLimitReached();
|
||||
});
|
||||
player.on('seeked', () => {
|
||||
checkVideoComplete();
|
||||
if (!player.hasStart) return;
|
||||
const clamped = clampTrialSeek();
|
||||
clearTrialSeekBlock();
|
||||
// 钳制后再补发进度:拖动松手时未必有 timeupdate
|
||||
if (pageActive.value) {
|
||||
emit('timeupdate', Number(player?.video?.currentTime) || 0);
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
suppressPlayAnalytics = false;
|
||||
}, 300);
|
||||
if (clamped) return;
|
||||
if (!playCheck()) {
|
||||
handleTrialLimitReached();
|
||||
}
|
||||
});
|
||||
player.on('seeking', () => {
|
||||
if (!player?.hasStart) return;
|
||||
suppressPlayAnalytics = true;
|
||||
if (needsTrialSeekLimit()) {
|
||||
setTrialFrameBlocked(true);
|
||||
}
|
||||
clampTrialSeek();
|
||||
});
|
||||
player.on('destroy', () => {
|
||||
if (destroySilently) return;
|
||||
addPlayCount();
|
||||
});
|
||||
};
|
||||
/** 次数请求序号:忽略过期响应,避免卸载/重挂竞态把 watchCount 盖成 0 */
|
||||
let freeCountReqSeq = 0;
|
||||
const isFreeCountRequestCanceled = (err: unknown) => {
|
||||
if (!err || typeof err !== 'object') return false;
|
||||
const e = err as { code?: string; name?: string; message?: string };
|
||||
return (
|
||||
e.code === 'ERR_CANCELED' ||
|
||||
e.name === 'CanceledError' ||
|
||||
e.name === 'AbortError' ||
|
||||
/cancel/i.test(String(e.message || ''))
|
||||
);
|
||||
};
|
||||
/**
|
||||
* @description 获取用户免费观看次数
|
||||
*/
|
||||
const getFreeWatchCount = () => {
|
||||
const reqSeq = ++freeCountReqSeq;
|
||||
VideoApi.getPlayCount(videoInfo.value.id)
|
||||
.then((userCount) => {
|
||||
if (reqSeq !== freeCountReqSeq) return;
|
||||
// 必须抛接口原值:finally 里读 ref 在取消/竞态时可能仍是初始 { watchCount: 0 }
|
||||
userFreeCount.value = userCount;
|
||||
setSharedFreeWatchCount({
|
||||
watchCount: Number(userCount?.watchCount),
|
||||
isCan: userCount?.isCan,
|
||||
videoInfo: videoInfo.value,
|
||||
});
|
||||
emit('getFreeCount', userCount);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (reqSeq !== freeCountReqSeq) return;
|
||||
// 取消不兜底 0,避免 Network 已返回 3 却被后续 cancel 覆盖
|
||||
if (isFreeCountRequestCanceled(err)) return;
|
||||
// 请求失败按无免费次数兜底,保证父组件仍能展示跳过预览入口
|
||||
emit('getFreeCount', { isCan: false, watchCount: 0 });
|
||||
})
|
||||
.finally(() => {
|
||||
if (reqSeq !== freeCountReqSeq) return;
|
||||
// 无论成功失败都初始化播放器,避免接口异常导致无法播放
|
||||
initVideo();
|
||||
});
|
||||
};
|
||||
const playing = () => {
|
||||
if (!state.playTimer) {
|
||||
state.playTimer = setInterval(() => {
|
||||
state.playTime++;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 获取播放链接
|
||||
*/
|
||||
const getPath = () => {
|
||||
const { baseUrl } = useConfigStore();
|
||||
return `${baseUrl}/api/app/vid/h5/m3u8/${videoUrl.value}?token=${getToken()}&c=${globalStore.videoRoadLine.url}`;
|
||||
};
|
||||
|
||||
/** 当前视频是否需要试看时长限制(不依赖 video 元素) */
|
||||
const isTrialPlaybackLimited = () => {
|
||||
if (isPreview.value) return true;
|
||||
if (videoInfo.value.vidStatus.hasPaid === true) return false;
|
||||
if (videoInfo.value.freeArea === true) return false;
|
||||
if (isVip && ((userInfo.vipLevel >= 2 && !props.hasDarkWebTag) || (userInfo.vipLevel == 4 && props.hasDarkWebTag))) {
|
||||
return false;
|
||||
}
|
||||
if (videoInfo.value.originCoins != 0 && videoInfo.value.coins == 0) return false;
|
||||
if (videoInfo.value.originCoins === 0 && videoInfo.value.freeArea === false && isVip) return false;
|
||||
if (userInfo.uid === videoInfo.value.publisher.uid) return false;
|
||||
if (userFreeCount.value.isCan === true && videoInfo.value.originCoins === 0) return false;
|
||||
if (
|
||||
useGlobalStore().presaleObj.status != 0 &&
|
||||
globalStore.advanceStatus.privilegeLimit.hasLimit &&
|
||||
useGlobalStore().preSaleCoinVideoPlay
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const TRIAL_SEEK_EPSILON = 0.05;
|
||||
|
||||
const getTrialMinTime = () => (previewStartFlag.value && !isPreview.value ? videoInfo.value.previewStart || 0 : 0);
|
||||
|
||||
/** 后端试看上限(秒):freeTime,有 previewStart 时为 previewStart + freeTime;预览片为 duration-3 */
|
||||
const getBackendTrialLimitSeconds = (): number | null => {
|
||||
if (!isTrialPlaybackLimited()) return null;
|
||||
if (isPreview.value) {
|
||||
const duration = Number(player?.video?.duration) || 0;
|
||||
if (!duration) return null;
|
||||
return Math.max(0, duration - 3);
|
||||
}
|
||||
let limit = videoInfo.value.freeTime ?? 0;
|
||||
if (previewStartFlag.value) {
|
||||
limit += videoInfo.value.previewStart ?? 0;
|
||||
}
|
||||
return limit;
|
||||
};
|
||||
|
||||
/** 与 playCheck 一致:currentTime 须小于该值才可继续播放 */
|
||||
const getTrialMaxTime = (): number | null => {
|
||||
if (!player?.video) return null;
|
||||
return getBackendTrialLimitSeconds();
|
||||
};
|
||||
|
||||
const isSeekTimeBeyondTrialLimit = (time: number) => {
|
||||
const limit = getBackendTrialLimitSeconds();
|
||||
if (limit == null) return false;
|
||||
return time >= limit - TRIAL_SEEK_EPSILON;
|
||||
};
|
||||
|
||||
const isSeekTimeBeforeTrialMin = (time: number) => {
|
||||
const min = getTrialMinTime();
|
||||
return min > 0 && time < min - TRIAL_SEEK_EPSILON;
|
||||
};
|
||||
|
||||
const needsTrialSeekLimit = () => isTrialPlaybackLimited();
|
||||
|
||||
const setPlayerCurrentTime = (time: number) => {
|
||||
if (!player?.video) return;
|
||||
try {
|
||||
player.currentTime = time;
|
||||
} catch {
|
||||
try {
|
||||
player.video.currentTime = time;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
syncXgplayerSeekUi(player, time);
|
||||
};
|
||||
|
||||
const setTrialFrameBlocked = (blocked: boolean) => {
|
||||
trialSeekBlocking.value = blocked;
|
||||
setTrialVideoFrameHidden(player, blocked);
|
||||
};
|
||||
|
||||
/** 试看结束弹窗需要透出暂停画面,拖动防闪帧遮罩需在弹窗出现后撤掉 */
|
||||
const revealTrialFrameForMask = () => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setTrialFrameBlocked(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const clearTrialSeekBlock = () => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!player?.video) return;
|
||||
if (getTrialMaxTime() == null) {
|
||||
setTrialFrameBlocked(false);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isSeekTimeBeyondTrialLimit(player.video.currentTime) &&
|
||||
!isSeekTimeBeforeTrialMin(player.video.currentTime)
|
||||
) {
|
||||
setTrialFrameBlocked(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleTrialSeekOverflow = (beyondMax: boolean): boolean => {
|
||||
const limit = getBackendTrialLimitSeconds();
|
||||
if (limit == null || !player?.video) return false;
|
||||
const min = getTrialMinTime();
|
||||
const snapTime = beyondMax ? Math.max(min, limit - TRIAL_SEEK_EPSILON) : min;
|
||||
setTrialFrameBlocked(true);
|
||||
player.pause();
|
||||
setPlayerCurrentTime(snapTime);
|
||||
player.config.disableProgress = true;
|
||||
// 进度条松手后 xgplayer 还会改一次 DOM,延迟再同步时间与进度
|
||||
requestAnimationFrame(() => syncXgplayerSeekUi(player, snapTime));
|
||||
if (beyondMax) {
|
||||
trialLimitHandled = true;
|
||||
revealTrialFrameForMask();
|
||||
emitTrialUnlockPopup();
|
||||
} else {
|
||||
revealTrialFrameForMask();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** @returns 是否发生试看越界并已回弹 */
|
||||
const clampTrialSeek = (): boolean => {
|
||||
if (!player?.video) return false;
|
||||
const t = player.video.currentTime;
|
||||
if (isSeekTimeBeyondTrialLimit(t)) return handleTrialSeekOverflow(true);
|
||||
if (isSeekTimeBeforeTrialMin(t)) return handleTrialSeekOverflow(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
const bindTrialProgressGuard = () => {
|
||||
unbindTrialProgressGuard?.();
|
||||
unbindTrialProgressGuard = bindXgTrialProgressGuard({
|
||||
getPlayer: () => player,
|
||||
needsTrialSeekLimit,
|
||||
getBackendTrialLimitSeconds,
|
||||
getTrialMinTime,
|
||||
isSeekTimeBeyondTrialLimit,
|
||||
handleTrialSeekOverflow,
|
||||
setPlayerCurrentTime,
|
||||
setTrialFrameBlocked,
|
||||
clearTrialSeekBlock,
|
||||
trialSeekEpsilon: TRIAL_SEEK_EPSILON,
|
||||
maskOnDragStart: () => isPreview.value,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 检测视频是否可以播放
|
||||
*/
|
||||
const playCheck = () => {
|
||||
let freeTime = videoInfo.value.freeTime || 0;
|
||||
if (isPreview.value) {
|
||||
if (player.video.currentTime < player.video.duration - 3) {
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
} else {
|
||||
player.config.disableProgress = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
//视频已购买
|
||||
if (videoInfo.value.vidStatus.hasPaid === true) {
|
||||
player.config.disableProgress = false;
|
||||
state.hideProgress = false;
|
||||
return true;
|
||||
}
|
||||
//免费视频
|
||||
if (videoInfo.value.freeArea === true) {
|
||||
state.hideProgress = false;
|
||||
return true;
|
||||
}
|
||||
// vip>2可以直接观看
|
||||
if (
|
||||
isVip &&
|
||||
((userInfo.vipLevel >= 2 && !props.hasDarkWebTag) || (userInfo.vipLevel == 4 && props.hasDarkWebTag))
|
||||
) {
|
||||
player.config.disableProgress = false;
|
||||
state.hideProgress = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
//金币视频免费看权限
|
||||
if (videoInfo.value.originCoins != 0 && videoInfo.value.coins == 0) {
|
||||
player.config.disableProgress = false;
|
||||
state.hideProgress = false;
|
||||
return true;
|
||||
}
|
||||
// 会员视频(金币为0,freeArea = false)
|
||||
if (videoInfo.value.originCoins === 0 && videoInfo.value.freeArea === false && isVip) {
|
||||
state.hideProgress = false;
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
}
|
||||
//自己发布的视频
|
||||
if (userInfo.uid === videoInfo.value.publisher.uid) {
|
||||
state.hideProgress = false;
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
}
|
||||
// VIP免费次数
|
||||
if (userFreeCount.value.isCan === true && videoInfo.value.originCoins === 0) {
|
||||
state.hideProgress = false;
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
}
|
||||
//免费时长
|
||||
if (previewStartFlag.value) {
|
||||
freeTime += videoInfo.value.previewStart;
|
||||
if (player.video.currentTime < videoInfo.value.previewStart) {
|
||||
player.config.disableProgress = true;
|
||||
return false;
|
||||
}
|
||||
if (player.video.currentTime < freeTime) {
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (player.video.currentTime < freeTime) {
|
||||
state.hideProgress = false;
|
||||
player.config.disableProgress = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 预售金币视频免费次数能否观看
|
||||
if (useGlobalStore().presaleObj.status != 0 && globalStore.advanceStatus.privilegeLimit.hasLimit) {
|
||||
if (useGlobalStore().preSaleCoinVideoPlay) {
|
||||
state.hideProgress = false;
|
||||
return true;
|
||||
}
|
||||
player.config.disableProgress = true;
|
||||
clearInterval(state.playTimer);
|
||||
state.playTimer = '';
|
||||
return false;
|
||||
}
|
||||
player.config.disableProgress = true;
|
||||
clearInterval(state.playTimer);
|
||||
state.playTimer = '';
|
||||
return false;
|
||||
}
|
||||
};
|
||||
//视频购买成功
|
||||
function success() {
|
||||
videoInfo.value.vidStatus.hasPaid = true;
|
||||
player.config.disableProgress = false;
|
||||
if (isPreview.value) {
|
||||
forceH264 = false;
|
||||
destroyPlayer(true);
|
||||
initVideo();
|
||||
// checkPreview();
|
||||
// player.src = videoUrl.value;
|
||||
}
|
||||
player.play();
|
||||
}
|
||||
/**
|
||||
* @description 获取token
|
||||
*/
|
||||
const getToken = () => {
|
||||
const { token } = useUserStore();
|
||||
return token;
|
||||
};
|
||||
|
||||
onDeactivated(() => {
|
||||
pageActive.value = false;
|
||||
destroyPlayer(true);
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
pageActive.value = true;
|
||||
if (!player && videoUrl.value) {
|
||||
initVideo();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
pageActive.value = false;
|
||||
if (player?.video) {
|
||||
addVideoHistory(props.videoInfo, HistoryVideoType.VIDEO, Math.round(player.video.currentTime));
|
||||
}
|
||||
destroyPlayer();
|
||||
});
|
||||
defineExpose({
|
||||
success,
|
||||
pause,
|
||||
play: () => {
|
||||
if (!player && videoUrl.value) initVideo();
|
||||
return player?.play?.();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
// @media (min-height: 700px) {
|
||||
// .vertical_video_warp {
|
||||
// padding-top: 18px;
|
||||
// }
|
||||
// }
|
||||
.vertical_video_warp {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.trial-seek-veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
background: #000;
|
||||
pointer-events: none;
|
||||
}
|
||||
:deep(.xgplayer-progress-played) {
|
||||
background: #fff;
|
||||
}
|
||||
:deep(.xgplayer-progress) {
|
||||
top: 9px;
|
||||
}
|
||||
// :deep(.xgplayer-start),
|
||||
:deep(.xgplayer-controls) {
|
||||
// transform: translate3d(0, 0, 0);
|
||||
// -webkit-transform: translate3d(0, 0, 0);
|
||||
z-index: 3;
|
||||
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
|
||||
}
|
||||
// :deep(.xgplayer-progress-btn) {
|
||||
// display: none !important;
|
||||
// }
|
||||
:deep(.xgplayer-progress) {
|
||||
left: 50%;
|
||||
right: 0;
|
||||
width: 54%;
|
||||
transform: translate(-61%, 0);
|
||||
}
|
||||
:deep(.xgplayer-time) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
span::after {
|
||||
content: '';
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
// :deep(.xgplayer-time-current::after) {
|
||||
// content: '';
|
||||
// display: none;
|
||||
// }
|
||||
.video-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
:deep(video),
|
||||
:deep(.xgplayer) {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
:deep(.xgplayer-enter-spinner) {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
.avoid_sliding {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 44px;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
}
|
||||
&[data-hide-progress='true'] {
|
||||
:deep(.xgplayer-progress),
|
||||
:deep(.xgplayer-time) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user