直播页:全屏包含底部弹幕区,可登录与发送
Made-with: Cursor
This commit is contained in:
@@ -18,53 +18,61 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="live-watch-status">{{ watchStatus }}</p>
|
<p class="live-watch-status">{{ watchStatus }}</p>
|
||||||
<div class="live-video-wrap">
|
<div ref="liveStageRef" class="live-stage">
|
||||||
<video
|
<div class="live-stage-media">
|
||||||
ref="watchVideoRef"
|
<div class="live-video-wrap">
|
||||||
class="live-room-video live-room-video--watch live-room-video--contain"
|
<video
|
||||||
playsinline
|
ref="watchVideoRef"
|
||||||
autoplay
|
class="live-room-video live-room-video--watch live-room-video--contain"
|
||||||
></video>
|
playsinline
|
||||||
<div class="live-dm-layer" aria-hidden="true">
|
autoplay
|
||||||
<div
|
></video>
|
||||||
v-for="d in dmItems"
|
<div class="live-dm-layer" aria-hidden="true">
|
||||||
:key="d.id"
|
<div
|
||||||
class="live-dm-line"
|
v-for="d in dmItems"
|
||||||
:style="{ top: d.top + '%' }"
|
:key="d.id"
|
||||||
>
|
class="live-dm-line"
|
||||||
<span class="live-dm-from">{{ d.from }}:</span>{{ d.text }}
|
:style="{ top: d.top + '%' }"
|
||||||
|
>
|
||||||
|
<span class="live-dm-from">{{ d.from }}:</span>{{ d.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制">
|
||||||
|
<button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button>
|
||||||
|
<button type="button" class="live-video-toolbtn" @click="toggleStageFullscreen">
|
||||||
|
{{ stageFullscreen ? '退出全屏' : '全屏' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制">
|
<div class="live-stage-footer">
|
||||||
<button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button>
|
<div class="live-dm-auth-row">
|
||||||
<button type="button" class="live-video-toolbtn" @click="toggleVideoFullscreen">全屏</button>
|
<template v-if="dmLoggedIn">
|
||||||
|
<span class="live-dm-user">已登录:{{ dmDisplayName }}</span>
|
||||||
|
<button type="button" class="live-dm-auth-btn" @click="logoutDmUser">退出</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<router-link class="live-dm-auth-link" to="/live/login">登录发弹幕</router-link>
|
||||||
|
<span class="live-dm-auth-sep">·</span>
|
||||||
|
<router-link class="live-dm-auth-link" to="/live/register">注册</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="live-dm-bar">
|
||||||
|
<input
|
||||||
|
v-model="dmDraft"
|
||||||
|
class="live-dm-input"
|
||||||
|
type="text"
|
||||||
|
maxlength="120"
|
||||||
|
:placeholder="dmLoggedIn ? '发条弹幕…' : '登录后可发弹幕(仍可观看)'"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!dmLoggedIn"
|
||||||
|
@keydown.enter.prevent="sendDm"
|
||||||
|
/>
|
||||||
|
<button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-dm-auth-row">
|
|
||||||
<template v-if="dmLoggedIn">
|
|
||||||
<span class="live-dm-user">已登录:{{ dmDisplayName }}</span>
|
|
||||||
<button type="button" class="live-dm-auth-btn" @click="logoutDmUser">退出</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<router-link class="live-dm-auth-link" to="/live/login">登录发弹幕</router-link>
|
|
||||||
<span class="live-dm-auth-sep">·</span>
|
|
||||||
<router-link class="live-dm-auth-link" to="/live/register">注册</router-link>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="live-dm-bar">
|
|
||||||
<input
|
|
||||||
v-model="dmDraft"
|
|
||||||
class="live-dm-input"
|
|
||||||
type="text"
|
|
||||||
maxlength="120"
|
|
||||||
:placeholder="dmLoggedIn ? '发条弹幕…' : '登录后可发弹幕(仍可观看)'"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!dmLoggedIn"
|
|
||||||
@keydown.enter.prevent="sendDm"
|
|
||||||
/>
|
|
||||||
<button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="live-block live-block--divider" aria-label="外链直播间">
|
<section class="live-block live-block--divider" aria-label="外链直播间">
|
||||||
@@ -92,6 +100,8 @@ import { startViewing, liveDanmakuWsURL } from '../utils/liveWebRTC'
|
|||||||
import { getSiteDmToken, getSiteDmUsername, clearSiteDmSession } from '../utils/siteUserAuth'
|
import { getSiteDmToken, getSiteDmUsername, clearSiteDmSession } from '../utils/siteUserAuth'
|
||||||
|
|
||||||
const watchVideoRef = ref(null)
|
const watchVideoRef = ref(null)
|
||||||
|
const liveStageRef = ref(null)
|
||||||
|
const stageFullscreen = ref(false)
|
||||||
const rawLiveUrl = ref('')
|
const rawLiveUrl = ref('')
|
||||||
const pageTitle = ref('视频直播')
|
const pageTitle = ref('视频直播')
|
||||||
const watchStatus = ref('正在检测本站直播…')
|
const watchStatus = ref('正在检测本站直播…')
|
||||||
@@ -177,20 +187,55 @@ function unmuteAndPlay() {
|
|||||||
v.play().catch(() => {})
|
v.play().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleVideoFullscreen() {
|
function syncStageFullscreenFlag() {
|
||||||
|
const d = document
|
||||||
|
const el = liveStageRef.value
|
||||||
|
if (!el) {
|
||||||
|
stageFullscreen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stageFullscreen.value =
|
||||||
|
d.fullscreenElement === el || d.webkitFullscreenElement === el
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整页舞台全屏:含画面 + 底部登录与发弹幕(非仅 video,避免全屏后看不到输入框) */
|
||||||
|
function toggleStageFullscreen() {
|
||||||
|
const stage = liveStageRef.value
|
||||||
const v = watchVideoRef.value
|
const v = watchVideoRef.value
|
||||||
if (!v) return
|
if (!stage) return
|
||||||
const doc = document
|
const doc = document
|
||||||
if (doc.fullscreenElement || doc.webkitFullscreenElement) {
|
const fsEl = doc.fullscreenElement || doc.webkitFullscreenElement
|
||||||
|
if (fsEl) {
|
||||||
doc.exitFullscreen?.() || doc.webkitExitFullscreen?.()
|
doc.exitFullscreen?.() || doc.webkitExitFullscreen?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (typeof v.webkitEnterFullscreen === 'function') {
|
if (typeof stage.requestFullscreen === 'function') {
|
||||||
v.webkitEnterFullscreen()
|
Promise.resolve(stage.requestFullscreen())
|
||||||
|
.then(() => syncStageFullscreenFlag())
|
||||||
|
.catch(() => {
|
||||||
|
if (typeof stage.webkitRequestFullscreen === 'function') {
|
||||||
|
try {
|
||||||
|
stage.webkitRequestFullscreen()
|
||||||
|
} catch (_) {
|
||||||
|
if (v && typeof v.webkitEnterFullscreen === 'function') v.webkitEnterFullscreen()
|
||||||
|
}
|
||||||
|
} else if (v && typeof v.webkitEnterFullscreen === 'function') {
|
||||||
|
v.webkitEnterFullscreen()
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const el = v
|
if (typeof stage.webkitRequestFullscreen === 'function') {
|
||||||
el.requestFullscreen?.() || el.webkitRequestFullscreen?.()
|
try {
|
||||||
|
stage.webkitRequestFullscreen()
|
||||||
|
} catch (_) {
|
||||||
|
if (v && typeof v.webkitEnterFullscreen === 'function') v.webkitEnterFullscreen()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (v && typeof v.webkitEnterFullscreen === 'function') {
|
||||||
|
v.webkitEnterFullscreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushDmLine(text, fromRaw) {
|
function pushDmLine(text, fromRaw) {
|
||||||
@@ -327,6 +372,8 @@ function sendDm() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
dmIntentionalClose = false
|
dmIntentionalClose = false
|
||||||
|
document.addEventListener('fullscreenchange', syncStageFullscreenFlag)
|
||||||
|
document.addEventListener('webkitfullscreenchange', syncStageFullscreenFlag)
|
||||||
loadCaptureQualityPref()
|
loadCaptureQualityPref()
|
||||||
loadHomepage()
|
loadHomepage()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -340,6 +387,8 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('fullscreenchange', syncStageFullscreenFlag)
|
||||||
|
document.removeEventListener('webkitfullscreenchange', syncStageFullscreenFlag)
|
||||||
dmIntentionalClose = true
|
dmIntentionalClose = true
|
||||||
dmSendQueue.length = 0
|
dmSendQueue.length = 0
|
||||||
if (dmReconnectTimer) {
|
if (dmReconnectTimer) {
|
||||||
@@ -432,11 +481,72 @@ onUnmounted(() => {
|
|||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
min-height: 1.4em;
|
min-height: 1.4em;
|
||||||
}
|
}
|
||||||
|
.live-stage {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
.live-video-wrap {
|
.live-video-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
.live-stage:fullscreen,
|
||||||
|
.live-stage:-webkit-full-screen {
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 14px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #0a0a12;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-stage-media,
|
||||||
|
.live-stage:-webkit-full-screen .live-stage-media {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-video-wrap,
|
||||||
|
.live-stage:-webkit-full-screen .live-video-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-room-video.live-room-video--contain,
|
||||||
|
.live-stage:-webkit-full-screen .live-room-video.live-room-video--contain {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-video-toolbar,
|
||||||
|
.live-stage:-webkit-full-screen .live-video-toolbar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-stage-footer,
|
||||||
|
.live-stage:-webkit-full-screen .live-stage-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
.live-stage:fullscreen .live-dm-auth-row,
|
||||||
|
.live-stage:fullscreen .live-dm-bar,
|
||||||
|
.live-stage:fullscreen .live-dm-hint,
|
||||||
|
.live-stage:-webkit-full-screen .live-dm-auth-row,
|
||||||
|
.live-stage:-webkit-full-screen .live-dm-bar,
|
||||||
|
.live-stage:-webkit-full-screen .live-dm-hint {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
.live-dm-layer {
|
.live-dm-layer {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user