Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
VITE_LIVEJSON_PROXY=
VITE_IMG_PROXY=
VITE_IFRAME_APP_URL=https://rmlive.scutbot.cn
VITE_CHATROOM_APP_ID=
VITE_CHATROOM_APP_KEY=
VITE_ENGAGEMENT_CHATROOM_ID=
# Only include engagement messages from the last N minutes (default 30)
VITE_ENGAGEMENT_QUERY_WINDOW_MINUTES=30
VITE_ENGAGEMENT_QUERY_WINDOW_MINUTES=30
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ Learn more about the recommended Project Setup and IDE Support in the [Vue Docs

## Environment

- `VITE_STATIC_PROXY`: optional static proxy base. Example: `https://schedule.scutbot.cn/static`
- `VITE_LIVEJSON_PROXY`: optional live-json proxy base. Example: `https://schedule.scutbot.cn/static`
- `VITE_IMG_PROXY`: optional image proxy base.
- `VITE_IFRAME_APP_URL`: app URL embedded by `pnpm build:iframe`. Defaults to `https://rmlive.scutbot.cn`.

When `VITE_STATIC_PROXY` is set, app requests are forwarded as:
When `VITE_LIVEJSON_PROXY` is set, app requests are forwarded as:

- live json: `https://schedule.scutbot.cn/static/https://rm-static.djicdn.com/live_json/*.json`
- team logos/images: `https://schedule.scutbot.cn/static/<original-image-url>`

The app does not append `/static` automatically. Please provide the full proxy base in `VITE_STATIC_PROXY`.
When `VITE_IMG_PROXY` is set, app requests are forwarded as:

- team logos/images: `https://schedule.scutbot.cn/static/<original-image-url>`

Live json requests automatically append a timestamp query string to avoid proxy cache.
The app does not append `/static` automatically. Please provide the full proxy base.
10 changes: 5 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
<link rel="preconnect" href="https://rm-static.djicdn.com" crossorigin />
<link rel="preconnect" href="https://schedule.scutbot.cn" crossorigin />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="RMLive - Better 直播间,提供更清晰的赛事状态、比分信息与直播观看体验。" />
<meta name="description" content="RMLive - 不一样的直播间,提供更清晰的赛事状态、比分信息与直播观看体验。" />
<meta name="theme-color" content="#0f172a" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="RMLive - Better 直播间" />
<meta property="og:site_name" content="RMLive - 不一样的直播间" />
<meta property="og:locale" content="zh_CN" />
<meta property="og:title" content="RMLive - Better 直播间" />
<meta property="og:title" content="RMLive - 不一样的直播间" />
<meta property="og:description" content="更清晰的赛事视图,更顺滑的直播体验。" />
<meta property="og:image" content="%BASE_URL%rmlive-share.svg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="RMLive - Better 直播间" />
<meta property="twitter:title" content="RMLive - 不一样的直播间" />
<meta property="twitter:description" content="更清晰的赛事视图,更顺滑的直播体验。" />
<meta property="twitter:image" content="%BASE_URL%rmlive-share.svg" />
<title>RMLive - Better 直播间</title>
<title>RMLive - 不一样的直播间</title>
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion public/rmlive-share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 49 additions & 2 deletions src/api/rmApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ export interface LiveQualityOption {
src: string;
}

export interface LivePerspectiveOption {
key: string;
label: string;
headimg: string | null;
qualities: LiveQualityOption[];
}

export interface LiveZoneOption {
zoneId: string;
zoneName: string;
Expand All @@ -175,6 +182,7 @@ export interface LiveZoneOption {
endAt: number | null;
zoneDates: string[];
qualities: LiveQualityOption[];
perspectives: LivePerspectiveOption[];
}

function toStartAt(value: unknown): number | null {
Expand Down Expand Up @@ -265,6 +273,40 @@ export function extractLiveZones(data: LiveGameInfo | null): LiveZoneOption[] {
const qualities = source
.map((item, qualityIndex) => toQualityOption(item, qualityIndex))
.filter((item): item is LiveQualityOption => item !== null);
const perspectives: LivePerspectiveOption[] = [
{
key: 'main',
label: '主视角',
headimg: null,
qualities,
},
];

if (Array.isArray(zone.fpvData)) {
zone.fpvData.forEach((item, perspectiveIndex) => {
const perspectiveQualities = Array.isArray(item.sources)
? item.sources
.map((sourceItem, qualityIndex) => toQualityOption(sourceItem, qualityIndex))
.filter((quality): quality is LiveQualityOption => quality !== null)
: [];
if (!perspectiveQualities.length) {
return;
}

const label =
typeof item.role === 'string' && item.role.trim()
? item.role.trim()
: `第一视角 ${perspectiveIndex + 1}`;
const headimg = typeof item.headimg === 'string' && item.headimg.trim() ? item.headimg.trim() : null;

perspectives.push({
key: `fpv-${perspectiveIndex}`,
label,
headimg,
qualities: perspectiveQualities,
});
});
}

return {
zoneId,
Expand All @@ -275,6 +317,7 @@ export function extractLiveZones(data: LiveGameInfo | null): LiveZoneOption[] {
endAt: endAt ?? dateEndAt,
zoneDates,
qualities,
perspectives,
};
})
.sort((a, b) => {
Expand Down Expand Up @@ -323,6 +366,7 @@ export function resolveLiveStreamUrl(
data: LiveGameInfo | null,
zoneId: string | null,
qualityRes: string | null,
perspectiveKey = 'main',
): string | null {
const zones = extractLiveZones(data);
if (!zones.length) {
Expand All @@ -334,7 +378,10 @@ export function resolveLiveStreamUrl(
const z = normalizeZoneId(zoneId);
const selectedZone =
zones.find((item) => normalizeZoneId(item.zoneId) === z) ?? zones[0];
const selectedQuality = selectedZone.qualities.find((item) => item.res === qualityRes);
const selectedPerspective =
selectedZone.perspectives.find((item) => item.key === perspectiveKey) ?? selectedZone.perspectives[0];
const qualities = selectedPerspective?.qualities ?? selectedZone.qualities;
const selectedQuality = qualities.find((item) => item.res === qualityRes);

return selectedQuality?.src ?? selectedZone.qualities[0]?.src ?? null;
return selectedQuality?.src ?? qualities[0]?.src ?? null;
}
2 changes: 1 addition & 1 deletion src/components/header/TopToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const settingsVisible = ref(false);
<img :src="brandLogoUrl" alt="RMLive logo" class="brand-logo" />
<div class="toolbar-brand-meta" v-if="!uiStore.isMobile">
<h1>
<span>RMLive - Better 直播流</span>
<span>RMLive - 不一样的直播间</span>
<small v-if="scheduleEventTitle" class="event-title">{{ scheduleEventTitle }}</small>
</h1>
<p>更清晰的赛事视图,更顺滑的直播体验</p>
Expand Down
19 changes: 19 additions & 0 deletions src/components/layout/LiveStage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const {
streamLoading,
liveGameInfo,
effectiveStreamErrorMessage,
playerPerspectiveOptions,
playerQualityOptions,
selectedPerspectiveKey,
selectedQualityRes,
selectedZoneChatRoomId,
runningMatchForSelectedZone,
Expand All @@ -42,6 +44,14 @@ function onRetry() {
void dataStore.retryLiveStream();
}

function onPerspectiveChange(value: string) {
dataStore.selectPerspective(value);
}

function onQualityChange(value: string) {
dataStore.selectQuality(value);
}

function onDanmu(msg: DanmuMessage) {
emit('danmu', msg);
}
Expand All @@ -62,10 +72,14 @@ function onDanmuReset() {
:stream-url="effectiveStreamUrl"
:loading="streamLoading"
:error-message="effectiveStreamErrorMessage"
:perspective-options="playerPerspectiveOptions"
:selected-perspective-key="selectedPerspectiveKey"
:quality-options="playerQualityOptions"
:selected-quality-res="selectedQualityRes"
:chat-room-id="selectedZoneChatRoomId"
@retry="onRetry"
@perspective-change="onPerspectiveChange"
@quality-change="onQualityChange"
@danmu="onDanmu"
@danmu-reset="onDanmuReset"
/>
Expand All @@ -90,10 +104,14 @@ function onDanmuReset() {
:stream-url="effectiveStreamUrl"
:loading="streamLoading"
:error-message="effectiveStreamErrorMessage"
:perspective-options="playerPerspectiveOptions"
:selected-perspective-key="selectedPerspectiveKey"
:quality-options="playerQualityOptions"
:selected-quality-res="selectedQualityRes"
:chat-room-id="selectedZoneChatRoomId"
@retry="onRetry"
@perspective-change="onPerspectiveChange"
@quality-change="onQualityChange"
@danmu="onDanmu"
@danmu-reset="onDanmuReset"
/>
Expand Down Expand Up @@ -153,4 +171,5 @@ function onDanmuReset() {
height: 13rem;
}
}

</style>
Loading
Loading