feat: 前端预览资源切换到 CDN 域名 airflow-play.airlabs.art
新增 rewriteTosUrl 在渲染层把 airdrama-media.tos-cn-beijing.volces.com 替换成 airflow-play.airlabs.art,覆盖 <video>/<audio> src 及 tosThumb 图片缩略;下载仍走原 TOS 直连域名以避开 CDN CORS 配置依赖。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5da67435b2
commit
bc47bd09c4
@ -4,7 +4,7 @@ import type { GenerationTask } from '../types';
|
||||
import { useGenerationStore } from '../store/generation';
|
||||
import { showToast } from './Toast';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
import { tosThumb } from '../lib/api';
|
||||
import { tosThumb, rewriteTosUrl } from '../lib/api';
|
||||
import styles from './GenerationCard.module.css';
|
||||
|
||||
const EditIcon = () => (
|
||||
@ -299,7 +299,7 @@ export function GenerationCard({ task, onOpenDetail }: Props) {
|
||||
onMouseLeave={() => setRefPreview(null)}
|
||||
>
|
||||
{ref.type === 'video' ? (
|
||||
<video src={ref.previewUrl} className={styles.refMedia} muted />
|
||||
<video src={rewriteTosUrl(ref.previewUrl)} className={styles.refMedia} muted />
|
||||
) : ref.type === 'audio' ? (
|
||||
<div className={styles.audioThumb}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
||||
@ -422,7 +422,7 @@ export function GenerationCard({ task, onOpenDetail }: Props) {
|
||||
{refPreview && createPortal(
|
||||
<div className={styles.mentionPreview} style={{ top: refPreview.top, left: refPreview.left }}>
|
||||
{refPreview.type === 'video' ? (
|
||||
<video src={refPreview.url} className={styles.mentionPreviewImg} autoPlay loop muted playsInline />
|
||||
<video src={rewriteTosUrl(refPreview.url)} className={styles.mentionPreviewImg} autoPlay loop muted playsInline />
|
||||
) : (
|
||||
<img src={tosThumb(refPreview.url, 300)} alt={refPreview.label} className={styles.mentionPreviewImg} />
|
||||
)}
|
||||
@ -460,7 +460,7 @@ export function GenerationCard({ task, onOpenDetail }: Props) {
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={task.resultUrl}
|
||||
src={rewriteTosUrl(task.resultUrl)}
|
||||
className={styles.resultMedia}
|
||||
loop
|
||||
preload="metadata"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useInputBarStore } from '../store/inputBar';
|
||||
import { assetsApi, tosThumb } from '../lib/api';
|
||||
import { assetsApi, tosThumb, rewriteTosUrl } from '../lib/api';
|
||||
import type { UploadedFile, AssetSearchResult } from '../types';
|
||||
import { parseAssetMentionsFromDOM } from '../lib/assetMentions';
|
||||
import { showToast } from './Toast';
|
||||
@ -694,7 +694,7 @@ export function PromptInput() {
|
||||
>
|
||||
<div className={styles.mentionThumb}>
|
||||
{ref.type === 'video' ? (
|
||||
<video src={ref.previewUrl} muted className={styles.thumbMedia} />
|
||||
<video src={rewriteTosUrl(ref.previewUrl)} muted className={styles.thumbMedia} />
|
||||
) : ref.type === 'audio' ? (
|
||||
<span style={{ fontSize: 16 }}>{'\u266B'}</span>
|
||||
) : (
|
||||
@ -752,7 +752,7 @@ export function PromptInput() {
|
||||
>
|
||||
{hoverRef.type === 'video' ? (
|
||||
<video
|
||||
src={hoverRef.previewUrl}
|
||||
src={rewriteTosUrl(hoverRef.previewUrl)}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
|
||||
@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
|
||||
import { useInputBarStore } from '../store/inputBar';
|
||||
import { showToast } from './Toast';
|
||||
import { ImageLightbox } from './ImageLightbox';
|
||||
import { tosThumb } from '../lib/api';
|
||||
import { tosThumb, rewriteTosUrl } from '../lib/api';
|
||||
import styles from './UniversalUpload.module.css';
|
||||
|
||||
const Spinner = () => (
|
||||
@ -143,7 +143,7 @@ export function UniversalUpload() {
|
||||
>
|
||||
<div className={styles.thumbInner}>
|
||||
{ref.type === 'video' ? (
|
||||
<video src={ref.previewUrl} className={styles.thumbMedia} muted />
|
||||
<video src={rewriteTosUrl(ref.previewUrl)} className={styles.thumbMedia} muted />
|
||||
) : ref.type === 'audio' ? (
|
||||
<div className={styles.audioPlaceholder}>
|
||||
<AudioIcon />
|
||||
|
||||
@ -6,7 +6,7 @@ import { ConfirmModal } from './ConfirmModal';
|
||||
import { ImageLightbox } from './ImageLightbox';
|
||||
import { useInputBarStore } from '../store/inputBar';
|
||||
import { renderPromptWithMentions } from './GenerationCard';
|
||||
import { tosThumb } from '../lib/api';
|
||||
import { tosThumb, rewriteTosUrl } from '../lib/api';
|
||||
import styles from './VideoDetailModal.module.css';
|
||||
|
||||
interface Props {
|
||||
@ -301,7 +301,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={task.resultUrl}
|
||||
src={rewriteTosUrl(task.resultUrl)}
|
||||
className={styles.video}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
@ -486,7 +486,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
|
||||
<div key={ref.id} className={styles.refItem}>
|
||||
<div style={{ position: 'relative', width: 56, height: 56 }}>
|
||||
{ref.type === 'video' ? (
|
||||
<video src={ref.previewUrl} className={styles.refImg} muted style={{ cursor: 'pointer' }} onClick={() => ref.previewUrl && setRefMediaPreview({ url: ref.previewUrl, type: 'video' })} />
|
||||
<video src={rewriteTosUrl(ref.previewUrl)} className={styles.refImg} muted style={{ cursor: 'pointer' }} onClick={() => ref.previewUrl && setRefMediaPreview({ url: ref.previewUrl, type: 'video' })} />
|
||||
) : ref.type === 'audio' ? (
|
||||
<div className={styles.refAudioPlaceholder} style={{ cursor: 'pointer' }} onClick={() => ref.previewUrl && setRefMediaPreview({ url: ref.previewUrl, type: 'audio' })}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
||||
@ -569,11 +569,11 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
|
||||
<div style={{ position: 'relative', background: '#111118', borderRadius: 12, padding: 24, border: '1px solid #2a2a38' }} onClick={(e) => e.stopPropagation()}>
|
||||
<button style={{ position: 'absolute', top: 8, right: 12, background: 'none', border: 'none', color: '#888', fontSize: 16, cursor: 'pointer' }} onClick={() => setRefMediaPreview(null)}>✕</button>
|
||||
{refMediaPreview.type === 'video' ? (
|
||||
<video src={refMediaPreview.url} controls autoPlay style={{ maxWidth: '80vw', maxHeight: '70vh', borderRadius: 8 }} />
|
||||
<video src={rewriteTosUrl(refMediaPreview.url)} controls autoPlay style={{ maxWidth: '80vw', maxHeight: '70vh', borderRadius: 8 }} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '20px 40px', color: '#888' }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>♫</div>
|
||||
<audio src={refMediaPreview.url} controls autoPlay style={{ width: 320 }} />
|
||||
<audio src={rewriteTosUrl(refMediaPreview.url)} controls autoPlay style={{ width: 320 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -434,6 +434,20 @@ export const assetsApi = {
|
||||
api.get<{ id: number; status: string; url: string; error_message: string }>(`/assets/${id}/status`),
|
||||
};
|
||||
|
||||
const TOS_ORIGIN = 'https://airdrama-media.tos-cn-beijing.volces.com';
|
||||
const PREVIEW_ORIGIN = 'https://airflow-play.airlabs.art';
|
||||
|
||||
/**
|
||||
* 把 TOS 直连域名换成 CDN 预览域名,仅用于 <video>/<audio>/<img> 等浏览器拉流场景。
|
||||
* 下载(fetch / a.href download)继续使用原 TOS 域名以避免 CDN CORS 配置依赖。
|
||||
* 非我们桶 / blob: / 已是预览域名 → 原样返回。
|
||||
*/
|
||||
export function rewriteTosUrl(url: string | undefined): string {
|
||||
if (!url) return '';
|
||||
if (!url.startsWith(TOS_ORIGIN)) return url;
|
||||
return PREVIEW_ORIGIN + url.slice(TOS_ORIGIN.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append TOS image resize parameter to reduce loading size.
|
||||
* Only applies to TOS image URLs (volces.com with image extensions).
|
||||
@ -443,8 +457,9 @@ export function tosThumb(url: string | undefined, height: number): string {
|
||||
// 只对我们自己的 TOS 桶生效(airdrama-media),不处理火山内部桶(ark-media-asset 等)
|
||||
if (!url.includes('airdrama-media')) return url;
|
||||
if (!/\.(png|jpg|jpeg|webp|gif)/i.test(url)) return url;
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
return `${url}${sep}x-tos-process=image/resize,h_${height}`;
|
||||
const rewritten = rewriteTosUrl(url);
|
||||
const sep = rewritten.includes('?') ? '&' : '?';
|
||||
return `${rewritten}${sep}x-tos-process=image/resize,h_${height}`;
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { adminApi } from '../lib/api';
|
||||
import { adminApi, rewriteTosUrl } from '../lib/api';
|
||||
import { VideoDetailModal } from '../components/VideoDetailModal';
|
||||
import type { AssetTeamSummary, AssetMemberSummary, AssetVideo, GenerationTask } from '../types';
|
||||
import styles from './AdminAssetsPage.module.css';
|
||||
@ -21,7 +21,7 @@ function VideoThumbnail({ video, onClick }: { video: AssetVideo; onClick: () =>
|
||||
onClick={onClick}
|
||||
>
|
||||
{video.result_url ? (
|
||||
<video ref={videoRef} src={video.result_url} className={styles.thumbVideo} muted loop preload="metadata" />
|
||||
<video ref={videoRef} src={rewriteTosUrl(video.result_url)} className={styles.thumbVideo} muted loop preload="metadata" />
|
||||
) : (
|
||||
<div className={styles.thumbPlaceholder} />
|
||||
)}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||
import { Sidebar } from '../components/Sidebar';
|
||||
import { VideoDetailModal } from '../components/VideoDetailModal';
|
||||
import { useGenerationStore } from '../store/generation';
|
||||
import { rewriteTosUrl } from '../lib/api';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import type { GenerationTask } from '../types';
|
||||
import styles from './AssetsPage.module.css';
|
||||
@ -70,7 +71,7 @@ function VideoThumbnail({
|
||||
{task.resultUrl ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={task.resultUrl}
|
||||
src={rewriteTosUrl(task.resultUrl)}
|
||||
className={styles.thumbVideo}
|
||||
muted
|
||||
loop
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { teamApi } from '../lib/api';
|
||||
import { teamApi, rewriteTosUrl } from '../lib/api';
|
||||
import { VideoDetailModal } from '../components/VideoDetailModal';
|
||||
import type { AssetMemberSummary, AssetVideo, GenerationTask } from '../types';
|
||||
import styles from './AdminAssetsPage.module.css';
|
||||
@ -21,7 +21,7 @@ function VideoThumbnail({ video, onClick }: { video: AssetVideo; onClick: () =>
|
||||
onClick={onClick}
|
||||
>
|
||||
{video.result_url ? (
|
||||
<video ref={videoRef} src={video.result_url} className={styles.thumbVideo} muted loop preload="metadata" />
|
||||
<video ref={videoRef} src={rewriteTosUrl(video.result_url)} className={styles.thumbVideo} muted loop preload="metadata" />
|
||||
) : (
|
||||
<div className={styles.thumbPlaceholder} />
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user