init
This commit is contained in:
305
hbuilder_app/pages/index/index.vue
Normal file
305
hbuilder_app/pages/index/index.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="panel">
|
||||
<text class="title">SyncTV</text>
|
||||
<text class="status">{{ status }}</text>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">Server</text>
|
||||
<input v-model="server" class="input" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">Room Code</text>
|
||||
<input v-model="roomCode" class="input" />
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<button class="primary" @click="createRoom">Create</button>
|
||||
<button @click="joinRoom">Join</button>
|
||||
</view>
|
||||
|
||||
<view class="divider" />
|
||||
|
||||
<text class="section">Source</text>
|
||||
<picker :range="sourceTypes" :value="sourceTypeIndex" @change="sourceTypeIndex = Number($event.detail.value)">
|
||||
<view class="select">Type: {{ sourceTypes[sourceTypeIndex] }}</view>
|
||||
</picker>
|
||||
<picker :range="sourceModes" :value="sourceModeIndex" @change="sourceModeIndex = Number($event.detail.value)">
|
||||
<view class="select">Connection: {{ sourceModes[sourceModeIndex] }}</view>
|
||||
</picker>
|
||||
<view class="field">
|
||||
<text class="label">URL</text>
|
||||
<input v-model="sourceUrl" class="input" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">Username</text>
|
||||
<input v-model="username" class="input" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">Password / Token</text>
|
||||
<input v-model="password" password class="input" />
|
||||
</view>
|
||||
<label class="checkbox">
|
||||
<checkbox :checked="isLive" @click="isLive = !isLive" />
|
||||
<text>Live stream</text>
|
||||
</label>
|
||||
<button :disabled="!isOwner" class="primary" @click="applySource">Apply Source</button>
|
||||
</view>
|
||||
|
||||
<view v-if="player.hasNative" class="native-player">
|
||||
<text class="native-player-text">VLC native player active</text>
|
||||
</view>
|
||||
<video
|
||||
v-else
|
||||
id="player"
|
||||
class="player"
|
||||
:src="playerUrl"
|
||||
controls
|
||||
autoplay="false"
|
||||
@timeupdate="onTimeUpdate"
|
||||
/>
|
||||
|
||||
<view class="controls">
|
||||
<button @click="play">Play</button>
|
||||
<button @click="pause">Pause</button>
|
||||
<button @click="syncToLive">Back To Live</button>
|
||||
</view>
|
||||
|
||||
<text class="error">{{ error }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { createApi } from '../../common/api'
|
||||
import { getDeviceId } from '../../common/device'
|
||||
import { createPlayer } from '../../common/player'
|
||||
import { SyncClient } from '../../common/sync'
|
||||
|
||||
const server = ref('http://yuyun-us1.stormrain.cn:8088')
|
||||
const roomCode = ref('')
|
||||
const status = ref('Not connected')
|
||||
const error = ref('')
|
||||
const isOwner = ref(false)
|
||||
const deviceId = getDeviceId()
|
||||
|
||||
const sourceTypes = ['url', 'oss', 'webdav', 'live']
|
||||
const sourceModes = ['direct', 'proxy']
|
||||
const sourceTypeIndex = ref(0)
|
||||
const sourceModeIndex = ref(0)
|
||||
const sourceUrl = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const isLive = ref(false)
|
||||
const playerUrl = ref('')
|
||||
const currentTimeMs = ref(0)
|
||||
const player = createPlayer({
|
||||
onError(message) {
|
||||
error.value = message
|
||||
}
|
||||
})
|
||||
|
||||
const sync = new SyncClient({
|
||||
onEvent: handleEvent,
|
||||
onError(message) {
|
||||
error.value = message
|
||||
}
|
||||
})
|
||||
|
||||
function api() {
|
||||
return createApi(server.value)
|
||||
}
|
||||
|
||||
async function createRoom() {
|
||||
try {
|
||||
error.value = ''
|
||||
const data = await api().createRoom()
|
||||
roomCode.value = data.room.code
|
||||
await joinRoom()
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
}
|
||||
}
|
||||
|
||||
async function joinRoom() {
|
||||
try {
|
||||
error.value = ''
|
||||
const snapshot = await api().getRoom(roomCode.value.toUpperCase(), deviceId)
|
||||
applySnapshot(snapshot)
|
||||
sync.connect(server.value, roomCode.value.toUpperCase(), deviceId)
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
}
|
||||
}
|
||||
|
||||
function applySource() {
|
||||
sync.setSource({
|
||||
type: sourceTypes[sourceTypeIndex.value],
|
||||
mode: sourceModes[sourceModeIndex.value],
|
||||
url: sourceUrl.value,
|
||||
isLive: isLive.value || sourceTypes[sourceTypeIndex.value] === 'live',
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
}
|
||||
|
||||
function play() {
|
||||
sync.play(currentTimeMs.value)
|
||||
if (!player.play()) {
|
||||
uni.createVideoContext('player').play()
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
sync.pause(currentTimeMs.value)
|
||||
if (!player.pause()) {
|
||||
uni.createVideoContext('player').pause()
|
||||
}
|
||||
}
|
||||
|
||||
function syncToLive() {
|
||||
sync.syncToLive()
|
||||
}
|
||||
|
||||
function onTimeUpdate(event) {
|
||||
currentTimeMs.value = Math.floor(event.detail.currentTime * 1000)
|
||||
}
|
||||
|
||||
function handleEvent(type, payload) {
|
||||
if (type === 'roomSnapshot' || type === 'sourceChanged') {
|
||||
applySnapshot(payload)
|
||||
} else if (type === 'playbackChanged') {
|
||||
applyPlayback(payload.playback, payload.serverNow)
|
||||
} else if (type === 'presenceChanged') {
|
||||
status.value = `${roomCode.value} · ${isOwner.value ? 'Owner' : 'Member'} · ${payload.onlineCount} online`
|
||||
} else if (type === 'error') {
|
||||
error.value = payload.message || 'unknown error'
|
||||
}
|
||||
}
|
||||
|
||||
function applySnapshot(snapshot) {
|
||||
roomCode.value = snapshot.room.code
|
||||
isOwner.value = snapshot.isOwner
|
||||
status.value = `${snapshot.room.code} · ${snapshot.isOwner ? 'Owner' : 'Member'} · ${snapshot.room.onlineCount} online`
|
||||
if (snapshot.room.source) {
|
||||
const source = snapshot.room.source
|
||||
sourceUrl.value = source.url
|
||||
isLive.value = !!source.isLive
|
||||
sourceTypeIndex.value = Math.max(0, sourceTypes.indexOf(source.type))
|
||||
sourceModeIndex.value = Math.max(0, sourceModes.indexOf(source.mode))
|
||||
playerUrl.value = player.open(source, server.value)
|
||||
}
|
||||
if (snapshot.room.playback) {
|
||||
applyPlayback(snapshot.room.playback, snapshot.serverNow)
|
||||
}
|
||||
}
|
||||
|
||||
function applyPlayback(playback, serverNow) {
|
||||
const ctx = uni.createVideoContext('player')
|
||||
if (playback.state === 'playing') {
|
||||
const target = playback.positionMs + Math.max(0, serverNow - playback.serverUpdatedAt)
|
||||
if (target > 0) {
|
||||
if (!player.seek(target)) ctx.seek(target / 1000)
|
||||
}
|
||||
if (!player.play()) ctx.play()
|
||||
} else if (playback.state === 'paused') {
|
||||
if (playback.positionMs > 0) {
|
||||
if (!player.seek(playback.positionMs)) ctx.seek(playback.positionMs / 1000)
|
||||
}
|
||||
if (!player.pause()) ctx.pause()
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sync.close()
|
||||
player.release()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
margin: 8rpx 0 24rpx;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.input,
|
||||
.select {
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.primary {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
.section {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 14rpx 0;
|
||||
}
|
||||
.player {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
margin-top: 24rpx;
|
||||
background: #000000;
|
||||
}
|
||||
.native-player {
|
||||
height: 420rpx;
|
||||
margin-top: 24rpx;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.native-player-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.error {
|
||||
display: block;
|
||||
color: #dc2626;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user