mirror of
https://github.com/langyuxiansheng/vue-aliplayer-v2.git
synced 2026-05-28 07:20:21 +08:00
1282 lines
40 KiB
Vue
1282 lines
40 KiB
Vue
<template>
|
|
<main class="demo-page">
|
|
<header class="app-header">
|
|
<div class="brand-block">
|
|
<span class="version">vue-aliplayer-v2</span>
|
|
<h1>Aliplayer Vue 3 Workbench</h1>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button type="button" class="button secondary" @click="resetAll">重置</button>
|
|
<button type="button" class="button secondary" @click="show = !show">{{ show ? '销毁' : '挂载' }}</button>
|
|
<button type="button" class="button primary" :disabled="isMultiple || !show" @click="playerRef?.retry(currentSource || undefined)">重试</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="status-strip">
|
|
<article>
|
|
<span>SDK</span>
|
|
<strong>{{ sdkVersion }}</strong>
|
|
</article>
|
|
<article>
|
|
<span>播放模式</span>
|
|
<strong>{{ sourceModeLabel }}</strong>
|
|
</article>
|
|
<article>
|
|
<span>推断格式</span>
|
|
<strong>{{ inferredFormat || '未识别' }}</strong>
|
|
</article>
|
|
<article>
|
|
<span>播放器状态</span>
|
|
<strong>{{ playerStatus }}</strong>
|
|
</article>
|
|
<article>
|
|
<span>事件</span>
|
|
<strong>{{ logs.length }}</strong>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="workspace">
|
|
<div class="stage-column">
|
|
<section class="player-panel">
|
|
<div class="player-panel-head">
|
|
<div>
|
|
<h2>{{ isMultiple ? '多实例验证' : '单实例调试' }}</h2>
|
|
<p>{{ currentSource || 'VID / STS 初始化模式' }}</p>
|
|
</div>
|
|
<label class="switch compact-switch">
|
|
<span>多实例</span>
|
|
<input v-model="isMultiple" type="checkbox">
|
|
</label>
|
|
</div>
|
|
|
|
<div class="player-frame" :class="{ 'is-multiple': isMultiple }">
|
|
<template v-if="show">
|
|
<div v-if="isMultiple" class="player-grid">
|
|
<VueAliplayerV2
|
|
v-for="item in multipleSources"
|
|
:key="item"
|
|
class="player"
|
|
:source="item"
|
|
:options="playerOptions"
|
|
:license="resolvedLicense"
|
|
:sdk-version="sdkVersion"
|
|
:component-scripts="componentScriptList"
|
|
:auto-format="autoFormat"
|
|
:normalize-source-url="normalizeSourceUrl"
|
|
:disable-tracking="disableTracking"
|
|
:low-latency="lowLatency"
|
|
:forbid-fast-forward="forbidFastForward"
|
|
@ready="handleReady"
|
|
@error="handleError"
|
|
@sdk-error="handleSdkError"
|
|
/>
|
|
</div>
|
|
<VueAliplayerV2
|
|
v-else
|
|
ref="playerRef"
|
|
class="player"
|
|
:source="currentSource"
|
|
:options="playerOptions"
|
|
:license="resolvedLicense"
|
|
:sdk-version="sdkVersion"
|
|
:component-scripts="componentScriptList"
|
|
:auto-format="autoFormat"
|
|
:normalize-source-url="normalizeSourceUrl"
|
|
:disable-tracking="disableTracking"
|
|
:low-latency="lowLatency"
|
|
:forbid-fast-forward="forbidFastForward"
|
|
@ready="handleReady"
|
|
@play="pushLog('play')"
|
|
@pause="pushLog('pause')"
|
|
@ended="pushLog('ended')"
|
|
@waiting="pushLog('waiting')"
|
|
@timeupdate="handleTimeUpdate"
|
|
@error="handleError"
|
|
@sdk-error="handleSdkError"
|
|
/>
|
|
</template>
|
|
<div v-else class="empty-player">
|
|
<strong>播放器未挂载</strong>
|
|
<span>{{ currentSource || '等待初始化参数' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="transport">
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="playerRef?.play()">播放</button>
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="playerRef?.pause()">暂停</button>
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="playerRef?.replay()">重播</button>
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="seekTo">跳转</button>
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="requestFullScreen">全屏</button>
|
|
<button type="button" class="button icon-button" :disabled="isMultiple || !show" @click="snapshotStatus">状态</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="lower-grid">
|
|
<article class="event-panel">
|
|
<div class="section-head">
|
|
<h2>事件流</h2>
|
|
<button type="button" class="text-button" @click="logs = []">清空</button>
|
|
</div>
|
|
<ol class="log-list">
|
|
<li v-for="item in logs" :key="item.id">
|
|
<time>{{ item.time }}</time>
|
|
<span>{{ item.message }}</span>
|
|
</li>
|
|
<li v-if="!logs.length" class="muted-row">暂无事件</li>
|
|
</ol>
|
|
</article>
|
|
|
|
<article class="config-panel">
|
|
<div class="section-head">
|
|
<h2>配置预览</h2>
|
|
<button type="button" class="text-button" @click="copyConfig">复制</button>
|
|
</div>
|
|
<pre>{{ formattedOptions }}</pre>
|
|
</article>
|
|
</section>
|
|
</div>
|
|
|
|
<aside class="inspector">
|
|
<div class="tabs" role="tablist" aria-label="参数分组">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.value"
|
|
type="button"
|
|
:class="{ active: activeTab === tab.value }"
|
|
@click="activeTab = tab.value"
|
|
>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<section v-if="activeTab === 'source'" class="panel-section">
|
|
<h2>播放源</h2>
|
|
<div class="segmented">
|
|
<button
|
|
v-for="mode in sourceModes"
|
|
:key="mode.value"
|
|
type="button"
|
|
:class="{ active: sourceMode === mode.value }"
|
|
@click="sourceMode = mode.value"
|
|
>
|
|
{{ mode.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<label v-if="sourceMode === 'url'" class="field">
|
|
<span>预设源</span>
|
|
<select v-model="selectedPreset">
|
|
<option v-for="item in sourcePresets" :key="item.value" :value="item.value">{{ item.label }}</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label v-if="sourceMode === 'url'" class="field">
|
|
<span>自定义 URL</span>
|
|
<input v-model="customSource" type="url" placeholder="https://example.com/video.mp4">
|
|
</label>
|
|
|
|
<div v-if="sourceMode === 'vid'" class="field-grid">
|
|
<label class="field">
|
|
<span>VID</span>
|
|
<input v-model="vidConfig.vid" type="text" placeholder="1e067a2831b641db...">
|
|
</label>
|
|
<label class="field">
|
|
<span>PlayAuth</span>
|
|
<input v-model="vidConfig.playauth" type="text" placeholder="Base64 PlayAuth">
|
|
</label>
|
|
<label class="field">
|
|
<span>Auth Timeout</span>
|
|
<input v-model.number="vidConfig.authTimeout" type="number" min="60" step="60">
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="sourceMode === 'sts'" class="field-grid">
|
|
<label class="field">
|
|
<span>VID</span>
|
|
<input v-model="stsConfig.vid" type="text">
|
|
</label>
|
|
<label class="field">
|
|
<span>Region</span>
|
|
<input v-model="stsConfig.region" type="text" placeholder="cn-shanghai">
|
|
</label>
|
|
<label class="field">
|
|
<span>AccessKey ID</span>
|
|
<input v-model="stsConfig.accessKeyId" type="text">
|
|
</label>
|
|
<label class="field">
|
|
<span>AccessKey Secret</span>
|
|
<input v-model="stsConfig.accessKeySecret" type="password">
|
|
</label>
|
|
<label class="field">
|
|
<span>Security Token</span>
|
|
<textarea v-model="stsConfig.securityToken" rows="3"></textarea>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="activeTab === 'basic'" class="panel-section">
|
|
<h2>基础</h2>
|
|
<div class="switch-grid">
|
|
<label class="switch">
|
|
<span>自动播放</span>
|
|
<input v-model="baseOptions.autoplay" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>直播</span>
|
|
<input v-model="baseOptions.isLive" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>H5</span>
|
|
<input v-model="baseOptions.useH5Prism" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>Flash</span>
|
|
<input v-model="baseOptions.useFlashPrism" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>循环</span>
|
|
<input v-model="baseOptions.rePlay" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>预加载</span>
|
|
<input v-model="baseOptions.preload" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>内联播放</span>
|
|
<input v-model="baseOptions.playsinline" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>禁用 Seek</span>
|
|
<input v-model="baseOptions.disableSeek" type="checkbox">
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field-grid">
|
|
<label class="field">
|
|
<span>宽度</span>
|
|
<input v-model="baseOptions.width" type="text">
|
|
</label>
|
|
<label class="field">
|
|
<span>高度</span>
|
|
<input v-model="baseOptions.height" type="text">
|
|
</label>
|
|
<label class="field">
|
|
<span>语言</span>
|
|
<select v-model="baseOptions.language">
|
|
<option value="zh-cn">zh-cn</option>
|
|
<option value="en-us">en-us</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>控制栏</span>
|
|
<select v-model="baseOptions.controlBarVisibility">
|
|
<option value="hover">hover</option>
|
|
<option value="click">click</option>
|
|
<option value="always">always</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>隐藏延迟</span>
|
|
<input v-model.number="baseOptions.showBarTime" type="number" min="0" step="500">
|
|
</label>
|
|
<label class="field">
|
|
<span>封面</span>
|
|
<input v-model="baseOptions.cover" type="url" placeholder="https://...">
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="activeTab === 'live'" class="panel-section">
|
|
<h2>直播与格式</h2>
|
|
<div class="switch-grid">
|
|
<label class="switch">
|
|
<span>自动格式</span>
|
|
<input v-model="autoFormat" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>低延迟</span>
|
|
<input v-model="lowLatency" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>URL 编码</span>
|
|
<input v-model="normalizeSourceUrl" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>禁快进</span>
|
|
<input v-model="forbidFastForward" type="checkbox">
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field-grid">
|
|
<label class="field">
|
|
<span>格式</span>
|
|
<select v-model="manualFormat">
|
|
<option value="">auto</option>
|
|
<option value="mp4">mp4</option>
|
|
<option value="m3u8">m3u8</option>
|
|
<option value="flv">flv</option>
|
|
<option value="mp3">mp3</option>
|
|
<option value="rtmp">rtmp</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>FLV stash</span>
|
|
<select v-model="flvStash">
|
|
<option value="auto">auto</option>
|
|
<option value="on">on</option>
|
|
<option value="off">off</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>stash size</span>
|
|
<input v-model.number="stashInitialSizeForFlv" type="number" min="0" step="32">
|
|
</label>
|
|
<label class="field">
|
|
<span>RTS 版本</span>
|
|
<input v-model="rtsVersion" type="text" placeholder="2.2.1">
|
|
</label>
|
|
<label class="field">
|
|
<span>时移开始</span>
|
|
<input v-model="liveStartTime" type="text" placeholder="2026/05/23 10:00:00">
|
|
</label>
|
|
<label class="field">
|
|
<span>时移结束</span>
|
|
<input v-model="liveOverTime" type="text" placeholder="2026/05/23 12:00:00">
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="activeTab === 'sdk'" class="panel-section">
|
|
<h2>SDK 与扩展</h2>
|
|
<div class="field-grid">
|
|
<label class="field">
|
|
<span>SDK 版本</span>
|
|
<input v-model="sdkVersion" type="text">
|
|
</label>
|
|
<label class="field">
|
|
<span>License 域名</span>
|
|
<input v-model="licenseForm.domain" type="text" placeholder="example.com">
|
|
</label>
|
|
<label class="field">
|
|
<span>License Key</span>
|
|
<input v-model="licenseForm.key" type="password">
|
|
</label>
|
|
<label class="field">
|
|
<span>组件脚本</span>
|
|
<textarea v-model="componentScriptsText" rows="3" placeholder="每行一个脚本地址"></textarea>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="switch-grid">
|
|
<label class="switch">
|
|
<span>拦截 track</span>
|
|
<input v-model="disableTracking" type="checkbox">
|
|
</label>
|
|
<label class="switch">
|
|
<span>系统菜单</span>
|
|
<input v-model="baseOptions.enableSystemMenu" type="checkbox">
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="activeTab === 'skin'" class="panel-section">
|
|
<h2>皮肤</h2>
|
|
<div class="field-grid">
|
|
<label class="field">
|
|
<span>大播放按钮</span>
|
|
<select v-model="skinPreset">
|
|
<option value="default">default</option>
|
|
<option value="center">center</option>
|
|
<option value="minimal">minimal</option>
|
|
<option value="hidden">hidden</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>试看时间</span>
|
|
<input v-model.number="previewTime" type="number" min="0">
|
|
</label>
|
|
<label class="field">
|
|
<span>打点秒数</span>
|
|
<input v-model="progressMarkersText" type="text" placeholder="10,30,60">
|
|
</label>
|
|
</div>
|
|
|
|
<label class="field">
|
|
<span>自定义 options JSON</span>
|
|
<textarea v-model="customOptionsJson" rows="8" spellcheck="false"></textarea>
|
|
</label>
|
|
<p v-if="customOptionsError" class="error-line">{{ customOptionsError }}</p>
|
|
</section>
|
|
</aside>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, reactive, ref } from 'vue';
|
|
import VueAliplayerV2, { type AliplayerLicense, type AliplayerOptions, type VueAliplayerV2Expose } from '../packages';
|
|
import { inferSourceFormat } from '../packages/AliplayerV2/source';
|
|
|
|
type SourceMode = 'url' | 'vid' | 'sts';
|
|
type TabValue = 'source' | 'basic' | 'live' | 'sdk' | 'skin';
|
|
type LogItem = {
|
|
id: string;
|
|
time: string;
|
|
message: string;
|
|
};
|
|
|
|
const playerRef = ref<VueAliplayerV2Expose | null>(null);
|
|
const show = ref(true);
|
|
const isMultiple = ref(false);
|
|
const activeTab = ref<TabValue>('source');
|
|
const sourceMode = ref<SourceMode>('url');
|
|
const selectedPreset = ref('//player.alicdn.com/video/aliyunmedia.mp4');
|
|
const customSource = ref('');
|
|
const playerStatus = ref('init');
|
|
const logs = ref<LogItem[]>([]);
|
|
const sdkVersion = ref('2.37.0');
|
|
const autoFormat = ref(true);
|
|
const lowLatency = ref(true);
|
|
const normalizeSourceUrl = ref(true);
|
|
const forbidFastForward = ref(false);
|
|
const disableTracking = ref(false);
|
|
const componentScriptsText = ref('');
|
|
const manualFormat = ref('');
|
|
const flvStash = ref<'auto' | 'on' | 'off'>('auto');
|
|
const stashInitialSizeForFlv = ref(128);
|
|
const rtsVersion = ref('');
|
|
const liveStartTime = ref('');
|
|
const liveOverTime = ref('');
|
|
const skinPreset = ref('center');
|
|
const previewTime = ref(0);
|
|
const progressMarkersText = ref('10,30,60');
|
|
const customOptionsJson = ref('{\n "diagnosisButtonVisible": true\n}');
|
|
const customOptionsError = ref('');
|
|
let logSeed = 0;
|
|
|
|
const tabs: Array<{ label: string; value: TabValue }> = [
|
|
{ label: '源', value: 'source' },
|
|
{ label: '基础', value: 'basic' },
|
|
{ label: '直播', value: 'live' },
|
|
{ label: 'SDK', value: 'sdk' },
|
|
{ label: '皮肤', value: 'skin' }
|
|
];
|
|
|
|
const sourceModes: Array<{ label: string; value: SourceMode }> = [
|
|
{ label: 'URL', value: 'url' },
|
|
{ label: 'VID', value: 'vid' },
|
|
{ label: 'STS', value: 'sts' }
|
|
];
|
|
|
|
const sourcePresets = [
|
|
{ label: 'MP4 演示源', value: '//player.alicdn.com/video/aliyunmedia.mp4' },
|
|
{ label: 'MP4 备用源', value: '//yunqivedio.alicdn.com/user-upload/nXPDX8AASx.mp4' },
|
|
{ label: 'M3U8 直播源', value: '//ivi.bupt.edu.cn/hls/cctv1.m3u8' }
|
|
];
|
|
|
|
const multipleSources = [
|
|
'//player.alicdn.com/video/aliyunmedia.mp4',
|
|
'//yunqivedio.alicdn.com/user-upload/nXPDX8AASx.mp4',
|
|
'//player.alicdn.com/video/aliyunmedia.mp4',
|
|
'//yunqivedio.alicdn.com/user-upload/nXPDX8AASx.mp4'
|
|
];
|
|
|
|
const baseOptions = reactive<AliplayerOptions>({
|
|
width: '100%',
|
|
height: '420px',
|
|
autoplay: true,
|
|
isLive: false,
|
|
useH5Prism: true,
|
|
useFlashPrism: false,
|
|
playsinline: true,
|
|
preload: true,
|
|
rePlay: false,
|
|
disableSeek: false,
|
|
enableSystemMenu: false,
|
|
controlBarVisibility: 'hover',
|
|
showBarTime: 5000,
|
|
language: 'zh-cn',
|
|
cover: ''
|
|
});
|
|
|
|
const licenseForm = reactive<AliplayerLicense>({
|
|
domain: '',
|
|
key: ''
|
|
});
|
|
|
|
const vidConfig = reactive({
|
|
vid: '',
|
|
playauth: '',
|
|
authTimeout: 7200
|
|
});
|
|
|
|
const stsConfig = reactive({
|
|
vid: '',
|
|
region: 'cn-shanghai',
|
|
accessKeyId: '',
|
|
accessKeySecret: '',
|
|
securityToken: ''
|
|
});
|
|
|
|
const sourceModeLabel = computed(() => sourceModes.find((item) => item.value === sourceMode.value)?.label || 'URL');
|
|
const currentSource = computed(() => {
|
|
if (sourceMode.value !== 'url') return null;
|
|
return customSource.value.trim() || selectedPreset.value;
|
|
});
|
|
const inferredFormat = computed(() => inferSourceFormat(currentSource.value));
|
|
const componentScriptList = computed(() => componentScriptsText.value.split('\n').map((item) => item.trim()).filter(Boolean));
|
|
const resolvedLicense = computed(() => {
|
|
if (!licenseForm.domain.trim() || !licenseForm.key.trim()) return null;
|
|
return {
|
|
domain: licenseForm.domain.trim(),
|
|
key: licenseForm.key.trim()
|
|
};
|
|
});
|
|
|
|
const skinLayout = computed(() => {
|
|
if (skinPreset.value === 'default') return undefined;
|
|
if (skinPreset.value === 'hidden') return false;
|
|
if (skinPreset.value === 'minimal') {
|
|
return [
|
|
{ name: 'bigPlayButton', align: 'cc' },
|
|
{
|
|
name: 'controlBar',
|
|
align: 'blabs',
|
|
x: 0,
|
|
y: 0,
|
|
children: [
|
|
{ name: 'progress', align: 'blabs', x: 0, y: 44 },
|
|
{ name: 'playButton', align: 'tl', x: 12, y: 12 },
|
|
{ name: 'timeDisplay', align: 'tl', x: 48, y: 10 },
|
|
{ name: 'fullScreenButton', align: 'tr', x: 12, y: 12 }
|
|
]
|
|
}
|
|
];
|
|
}
|
|
return [{ name: 'bigPlayButton', align: 'cc' }];
|
|
});
|
|
|
|
const progressMarkers = computed(() => progressMarkersText.value
|
|
.split(',')
|
|
.map((item) => Number(item.trim()))
|
|
.filter((item) => Number.isFinite(item) && item > 0)
|
|
.map((time, index) => ({
|
|
offset: time,
|
|
text: `Marker ${index + 1}`
|
|
})));
|
|
|
|
const parsedCustomOptions = computed<Record<string, unknown>>(() => {
|
|
customOptionsError.value = '';
|
|
if (!customOptionsJson.value.trim()) return {};
|
|
|
|
try {
|
|
return JSON.parse(customOptionsJson.value) as Record<string, unknown>;
|
|
} catch (error) {
|
|
customOptionsError.value = error instanceof Error ? error.message : String(error);
|
|
return {};
|
|
}
|
|
});
|
|
|
|
const playerOptions = computed<AliplayerOptions>(() => {
|
|
const options: AliplayerOptions = {
|
|
...baseOptions,
|
|
...parsedCustomOptions.value
|
|
};
|
|
|
|
if (!baseOptions.cover) delete options.cover;
|
|
if (manualFormat.value) options.format = manualFormat.value;
|
|
if (flvStash.value !== 'auto') options.enableStashBufferForFlv = flvStash.value === 'on';
|
|
if (stashInitialSizeForFlv.value > 0) options.stashInitialSizeForFlv = stashInitialSizeForFlv.value;
|
|
if (rtsVersion.value.trim()) options.rtsVersion = rtsVersion.value.trim();
|
|
if (liveStartTime.value.trim()) options.liveStartTime = liveStartTime.value.trim();
|
|
if (liveOverTime.value.trim()) options.liveOverTime = liveOverTime.value.trim();
|
|
if (skinLayout.value !== undefined) options.skinLayout = skinLayout.value;
|
|
if (previewTime.value > 0) options.previewTime = previewTime.value;
|
|
if (progressMarkers.value.length) options.progressMarkers = progressMarkers.value;
|
|
|
|
if (sourceMode.value === 'vid') {
|
|
options.vid = vidConfig.vid.trim();
|
|
options.playauth = vidConfig.playauth.trim();
|
|
options.authTimeout = vidConfig.authTimeout;
|
|
}
|
|
|
|
if (sourceMode.value === 'sts') {
|
|
options.vid = stsConfig.vid.trim();
|
|
options.region = stsConfig.region.trim();
|
|
options.accessKeyId = stsConfig.accessKeyId.trim();
|
|
options.accessKeySecret = stsConfig.accessKeySecret.trim();
|
|
options.securityToken = stsConfig.securityToken.trim();
|
|
}
|
|
|
|
return options;
|
|
});
|
|
|
|
const formattedOptions = computed(() => JSON.stringify({
|
|
source: currentSource.value,
|
|
options: playerOptions.value,
|
|
license: resolvedLicense.value,
|
|
sdkVersion: sdkVersion.value,
|
|
autoFormat: autoFormat.value,
|
|
lowLatency: lowLatency.value,
|
|
normalizeSourceUrl: normalizeSourceUrl.value,
|
|
disableTracking: disableTracking.value,
|
|
componentScripts: componentScriptList.value
|
|
}, null, 2));
|
|
|
|
function pushLog(message: string): void {
|
|
const item = {
|
|
id: `${Date.now()}-${logSeed++}`,
|
|
time: new Date().toLocaleTimeString(),
|
|
message
|
|
};
|
|
logs.value = [item, ...logs.value].slice(0, 10);
|
|
}
|
|
|
|
function handleReady(): void {
|
|
playerStatus.value = 'ready';
|
|
pushLog('ready');
|
|
}
|
|
|
|
function handleTimeUpdate(): void {
|
|
const current = playerRef.value?.getCurrentTime();
|
|
if (typeof current === 'number' && Math.floor(current) % 15 === 0) {
|
|
playerStatus.value = `playing ${Math.floor(current)}s`;
|
|
}
|
|
}
|
|
|
|
function handleError(event?: unknown): void {
|
|
playerStatus.value = 'error';
|
|
pushLog(`error ${JSON.stringify(event || {})}`);
|
|
}
|
|
|
|
function handleSdkError(error: Error): void {
|
|
playerStatus.value = 'sdk-error';
|
|
pushLog(`sdk-error ${error.message}`);
|
|
}
|
|
|
|
function seekTo(): void {
|
|
playerRef.value?.seek(15);
|
|
pushLog('seek 15s');
|
|
}
|
|
|
|
function requestFullScreen(): void {
|
|
playerRef.value?.requestFullScreen();
|
|
pushLog('fullscreen');
|
|
}
|
|
|
|
function snapshotStatus(): void {
|
|
playerStatus.value = playerRef.value?.getStatus() || 'unknown';
|
|
pushLog(`status ${playerStatus.value}`);
|
|
}
|
|
|
|
function copyConfig(): void {
|
|
void navigator.clipboard?.writeText(formattedOptions.value);
|
|
pushLog('config copied');
|
|
}
|
|
|
|
function resetAll(): void {
|
|
sourceMode.value = 'url';
|
|
selectedPreset.value = sourcePresets[0].value;
|
|
customSource.value = '';
|
|
sdkVersion.value = '2.37.0';
|
|
autoFormat.value = true;
|
|
lowLatency.value = true;
|
|
normalizeSourceUrl.value = true;
|
|
forbidFastForward.value = false;
|
|
disableTracking.value = false;
|
|
manualFormat.value = '';
|
|
flvStash.value = 'auto';
|
|
stashInitialSizeForFlv.value = 128;
|
|
rtsVersion.value = '';
|
|
liveStartTime.value = '';
|
|
liveOverTime.value = '';
|
|
skinPreset.value = 'center';
|
|
previewTime.value = 0;
|
|
progressMarkersText.value = '10,30,60';
|
|
customOptionsJson.value = '{\n "diagnosisButtonVisible": true\n}';
|
|
componentScriptsText.value = '';
|
|
licenseForm.domain = '';
|
|
licenseForm.key = '';
|
|
Object.assign(baseOptions, {
|
|
width: '100%',
|
|
height: '420px',
|
|
autoplay: true,
|
|
isLive: false,
|
|
useH5Prism: true,
|
|
useFlashPrism: false,
|
|
playsinline: true,
|
|
preload: true,
|
|
rePlay: false,
|
|
disableSeek: false,
|
|
enableSystemMenu: false,
|
|
controlBarVisibility: 'hover',
|
|
showBarTime: 5000,
|
|
language: 'zh-cn',
|
|
cover: ''
|
|
});
|
|
pushLog('reset');
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
:global(*) {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:global(body) {
|
|
margin: 0;
|
|
min-width: 320px;
|
|
overflow-x: hidden;
|
|
color: oklch(24% 0.018 235);
|
|
background:
|
|
linear-gradient(180deg, oklch(97% 0.012 225), oklch(94% 0.018 210) 58%, oklch(95% 0.011 145));
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
|
}
|
|
|
|
:global(button),
|
|
:global(input),
|
|
:global(select),
|
|
:global(textarea) {
|
|
font: inherit;
|
|
}
|
|
|
|
.demo-page {
|
|
width: min(1500px, calc(100vw - 32px));
|
|
margin: 0 auto;
|
|
padding: 22px 0 40px;
|
|
}
|
|
|
|
.app-header,
|
|
.status-strip,
|
|
.player-panel,
|
|
.event-panel,
|
|
.config-panel,
|
|
.inspector {
|
|
border: 1px solid oklch(84% 0.018 225);
|
|
background: oklch(99% 0.006 225);
|
|
box-shadow: 0 18px 48px oklch(40% 0.05 230 / 10%);
|
|
}
|
|
|
|
.app-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
padding: 18px 20px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.brand-block {
|
|
display: grid;
|
|
gap: 4px;
|
|
}
|
|
|
|
.version {
|
|
width: fit-content;
|
|
padding: 4px 8px;
|
|
color: oklch(40% 0.06 170);
|
|
background: oklch(94% 0.045 165);
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
p {
|
|
margin: 0;
|
|
}
|
|
|
|
h1 {
|
|
color: oklch(21% 0.02 235);
|
|
font-size: 26px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
h2 {
|
|
color: oklch(25% 0.018 235);
|
|
font-size: 16px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.header-actions,
|
|
.transport,
|
|
.section-head,
|
|
.tabs,
|
|
.segmented {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-actions,
|
|
.transport {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.button,
|
|
.text-button,
|
|
.tabs button,
|
|
.segmented button {
|
|
min-height: 36px;
|
|
border: 1px solid oklch(78% 0.024 225);
|
|
border-radius: 7px;
|
|
color: oklch(27% 0.02 235);
|
|
background: oklch(99% 0.004 225);
|
|
cursor: pointer;
|
|
transition: border-color 180ms ease-out, color 180ms ease-out, background 180ms ease-out, transform 180ms ease-out;
|
|
}
|
|
|
|
.button {
|
|
padding: 0 14px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.button:hover,
|
|
.text-button:hover,
|
|
.tabs button:hover,
|
|
.segmented button:hover {
|
|
border-color: oklch(59% 0.13 170);
|
|
color: oklch(36% 0.1 170);
|
|
}
|
|
|
|
.button:active,
|
|
.text-button:active,
|
|
.tabs button:active,
|
|
.segmented button:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.primary {
|
|
border-color: oklch(53% 0.13 170);
|
|
color: oklch(98% 0.006 170);
|
|
background: oklch(48% 0.12 170);
|
|
}
|
|
|
|
.secondary {
|
|
background: oklch(97% 0.01 225);
|
|
}
|
|
|
|
.icon-button {
|
|
min-width: 72px;
|
|
}
|
|
|
|
.text-button {
|
|
min-height: 30px;
|
|
padding: 0 10px;
|
|
color: oklch(39% 0.06 170);
|
|
background: transparent;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.status-strip {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 1px;
|
|
margin-top: 14px;
|
|
overflow: hidden;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.status-strip article {
|
|
display: grid;
|
|
gap: 6px;
|
|
min-height: 72px;
|
|
padding: 14px 16px;
|
|
background: oklch(98% 0.007 225);
|
|
}
|
|
|
|
.status-strip span,
|
|
.player-panel-head p,
|
|
.field span,
|
|
.switch span,
|
|
.log-list time {
|
|
color: oklch(48% 0.025 235);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.status-strip strong {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
color: oklch(24% 0.02 235);
|
|
font-size: 16px;
|
|
line-height: 1.2;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.workspace {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 430px;
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.stage-column {
|
|
display: grid;
|
|
gap: 16px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.player-panel,
|
|
.event-panel,
|
|
.config-panel,
|
|
.inspector {
|
|
min-width: 0;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.player-panel {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.player-panel-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 16px 18px;
|
|
border-bottom: 1px solid oklch(87% 0.016 225);
|
|
}
|
|
|
|
.player-panel-head > div {
|
|
display: grid;
|
|
min-width: 0;
|
|
gap: 4px;
|
|
}
|
|
|
|
.player-panel-head p {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.player-frame {
|
|
min-height: 460px;
|
|
padding: 14px;
|
|
overflow: hidden;
|
|
background:
|
|
radial-gradient(circle at top left, oklch(83% 0.06 172 / 20%), transparent 30%),
|
|
oklch(18% 0.015 235);
|
|
}
|
|
|
|
.player-frame.is-multiple {
|
|
min-height: 360px;
|
|
}
|
|
|
|
.player {
|
|
width: 100%;
|
|
min-height: 430px;
|
|
overflow: hidden;
|
|
background: oklch(13% 0.014 235);
|
|
border: 1px solid oklch(30% 0.025 235);
|
|
border-radius: 7px;
|
|
}
|
|
|
|
.player-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.player-grid .player {
|
|
min-height: 250px;
|
|
}
|
|
|
|
.empty-player {
|
|
display: grid;
|
|
min-height: 430px;
|
|
place-items: center;
|
|
align-content: center;
|
|
gap: 8px;
|
|
color: oklch(87% 0.01 225);
|
|
background: oklch(17% 0.014 235);
|
|
border: 1px dashed oklch(43% 0.025 235);
|
|
border-radius: 7px;
|
|
}
|
|
|
|
.empty-player span {
|
|
max-width: 70ch;
|
|
overflow: hidden;
|
|
color: oklch(70% 0.018 225);
|
|
font-size: 12px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.transport {
|
|
padding: 12px 14px 14px;
|
|
border-top: 1px solid oklch(87% 0.016 225);
|
|
}
|
|
|
|
.lower-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.event-panel,
|
|
.config-panel {
|
|
min-width: 0;
|
|
padding: 16px;
|
|
}
|
|
|
|
.section-head {
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.log-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
max-height: 290px;
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: auto;
|
|
list-style: none;
|
|
}
|
|
|
|
.log-list li {
|
|
display: grid;
|
|
grid-template-columns: 82px minmax(0, 1fr);
|
|
gap: 10px;
|
|
align-items: start;
|
|
min-height: 34px;
|
|
padding: 8px 10px;
|
|
color: oklch(28% 0.02 235);
|
|
background: oklch(97% 0.008 225);
|
|
border: 1px solid oklch(88% 0.014 225);
|
|
border-radius: 6px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.log-list span {
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.muted-row {
|
|
display: block !important;
|
|
color: oklch(52% 0.02 235);
|
|
font-family: inherit !important;
|
|
}
|
|
|
|
.config-panel pre {
|
|
max-height: 290px;
|
|
margin: 0;
|
|
padding: 12px;
|
|
overflow: auto;
|
|
color: oklch(88% 0.02 155);
|
|
background: oklch(18% 0.018 235);
|
|
border-radius: 7px;
|
|
font-size: 12px;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.inspector {
|
|
position: sticky;
|
|
top: 14px;
|
|
max-height: calc(100vh - 28px);
|
|
overflow: hidden;
|
|
align-self: start;
|
|
}
|
|
|
|
.tabs {
|
|
gap: 6px;
|
|
padding: 10px;
|
|
border-bottom: 1px solid oklch(87% 0.016 225);
|
|
background: oklch(96% 0.01 225);
|
|
}
|
|
|
|
.tabs button {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 0 8px;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.tabs button.active,
|
|
.segmented button.active {
|
|
border-color: oklch(54% 0.12 170);
|
|
color: oklch(31% 0.09 170);
|
|
background: oklch(93% 0.04 165);
|
|
}
|
|
|
|
.panel-section {
|
|
display: grid;
|
|
gap: 16px;
|
|
max-height: calc(100vh - 92px);
|
|
padding: 16px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.segmented {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 6px;
|
|
padding: 4px;
|
|
background: oklch(96% 0.01 225);
|
|
border: 1px solid oklch(86% 0.016 225);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.segmented button {
|
|
border-color: transparent;
|
|
background: transparent;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.field-grid,
|
|
.switch-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.field {
|
|
display: grid;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.field input,
|
|
.field select,
|
|
.field textarea {
|
|
width: 100%;
|
|
min-height: 38px;
|
|
padding: 0 10px;
|
|
color: oklch(24% 0.018 235);
|
|
background: oklch(99% 0.005 225);
|
|
border: 1px solid oklch(78% 0.024 225);
|
|
border-radius: 7px;
|
|
outline: none;
|
|
transition: border-color 160ms ease-out, box-shadow 160ms ease-out;
|
|
}
|
|
|
|
.field textarea {
|
|
min-height: 90px;
|
|
padding: 10px;
|
|
resize: vertical;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.field input:focus,
|
|
.field select:focus,
|
|
.field textarea:focus {
|
|
border-color: oklch(55% 0.12 170);
|
|
box-shadow: 0 0 0 3px oklch(78% 0.09 170 / 22%);
|
|
}
|
|
|
|
.switch {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
min-height: 42px;
|
|
padding: 10px 12px;
|
|
background: oklch(98% 0.007 225);
|
|
border: 1px solid oklch(86% 0.016 225);
|
|
border-radius: 7px;
|
|
}
|
|
|
|
.compact-switch {
|
|
min-width: 112px;
|
|
}
|
|
|
|
.switch input {
|
|
width: 18px;
|
|
height: 18px;
|
|
accent-color: oklch(49% 0.12 170);
|
|
}
|
|
|
|
.error-line {
|
|
padding: 10px 12px;
|
|
color: oklch(43% 0.13 28);
|
|
background: oklch(95% 0.04 35);
|
|
border: 1px solid oklch(82% 0.06 35);
|
|
border-radius: 7px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
@media (max-width: 1180px) {
|
|
.workspace,
|
|
.lower-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.inspector {
|
|
position: static;
|
|
max-height: none;
|
|
}
|
|
|
|
.panel-section {
|
|
max-height: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 780px) {
|
|
.demo-page {
|
|
width: min(100vw - 20px, 740px);
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.app-header {
|
|
align-items: stretch;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.header-actions,
|
|
.transport {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
width: 100%;
|
|
}
|
|
|
|
.header-actions .primary {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.button,
|
|
.icon-button {
|
|
min-width: 0;
|
|
padding-inline: 8px;
|
|
}
|
|
|
|
.player-panel-head {
|
|
align-items: stretch;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.status-strip,
|
|
.field-grid,
|
|
.switch-grid,
|
|
.player-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.player-frame,
|
|
.player,
|
|
.empty-player,
|
|
.player-grid .player {
|
|
min-height: 260px;
|
|
}
|
|
|
|
.tabs {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.tabs button {
|
|
min-width: 74px;
|
|
}
|
|
}
|
|
</style>
|