11 changed files with 187 additions and 18 deletions
@ -0,0 +1,82 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { |
|||
DEFAULT_VIDEO_POSTER_QUALITY, |
|||
buildVideoPosterUrl, |
|||
normalizeVideoPosterQuality, |
|||
readVideoPosterQualityConfig, |
|||
} from "@/utils/videoPosterUrl"; |
|||
|
|||
const VIDEO_URL = "https://statictest.popi.art/media/video/2026/0714/11768.mp4"; |
|||
|
|||
describe("buildVideoPosterUrl", () => { |
|||
it("appends the Tencent CI snapshot params with the default quality", () => { |
|||
expect(buildVideoPosterUrl(VIDEO_URL)).toBe( |
|||
`${VIDEO_URL}?ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/${DEFAULT_VIDEO_POSTER_QUALITY}` |
|||
); |
|||
}); |
|||
|
|||
it("uses the provided quality", () => { |
|||
expect(buildVideoPosterUrl(VIDEO_URL, 60)).toBe( |
|||
`${VIDEO_URL}?ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/60` |
|||
); |
|||
}); |
|||
|
|||
it("uses & separator when the url already has a query string", () => { |
|||
expect(buildVideoPosterUrl(`${VIDEO_URL}?token=abc`, 80)).toBe( |
|||
`${VIDEO_URL}?token=abc&ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/80` |
|||
); |
|||
}); |
|||
|
|||
it("falls back to the default quality when the value is out of range or invalid", () => { |
|||
const expected = `${VIDEO_URL}?ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/${DEFAULT_VIDEO_POSTER_QUALITY}`; |
|||
expect(buildVideoPosterUrl(VIDEO_URL, 0)).toBe(expected); |
|||
expect(buildVideoPosterUrl(VIDEO_URL, 200)).toBe(expected); |
|||
expect(buildVideoPosterUrl(VIDEO_URL, Number.NaN)).toBe(expected); |
|||
}); |
|||
|
|||
it("returns null for non-http urls and empty values", () => { |
|||
expect(buildVideoPosterUrl(null)).toBeNull(); |
|||
expect(buildVideoPosterUrl(undefined)).toBeNull(); |
|||
expect(buildVideoPosterUrl("")).toBeNull(); |
|||
expect(buildVideoPosterUrl("blob:https://example.com/abc")).toBeNull(); |
|||
expect(buildVideoPosterUrl("data:video/mp4;base64,abc")).toBeNull(); |
|||
}); |
|||
}); |
|||
|
|||
describe("normalizeVideoPosterQuality", () => { |
|||
it("accepts numbers and numeric strings within [1, 100]", () => { |
|||
expect(normalizeVideoPosterQuality(80)).toBe(80); |
|||
expect(normalizeVideoPosterQuality("60")).toBe(60); |
|||
expect(normalizeVideoPosterQuality(75.4)).toBe(75); |
|||
}); |
|||
|
|||
it("rejects out-of-range or invalid values", () => { |
|||
expect(normalizeVideoPosterQuality(0)).toBeNull(); |
|||
expect(normalizeVideoPosterQuality(101)).toBeNull(); |
|||
expect(normalizeVideoPosterQuality("abc")).toBeNull(); |
|||
expect(normalizeVideoPosterQuality(null)).toBeNull(); |
|||
expect(normalizeVideoPosterQuality(undefined)).toBeNull(); |
|||
}); |
|||
}); |
|||
|
|||
describe("readVideoPosterQualityConfig", () => { |
|||
it("reads ossVideoSnapshotQuality from nested system config data", () => { |
|||
expect( |
|||
readVideoPosterQualityConfig({ |
|||
data: { systemConfig: { ossVideoSnapshotQuality: 60 } }, |
|||
}) |
|||
).toBe(60); |
|||
}); |
|||
|
|||
it("reads ossVideoSnapshotQuality from top-level system config", () => { |
|||
expect( |
|||
readVideoPosterQualityConfig({ systemConfig: { ossVideoSnapshotQuality: "70" } }) |
|||
).toBe(70); |
|||
}); |
|||
|
|||
it("returns null when the quality is missing or invalid", () => { |
|||
expect(readVideoPosterQualityConfig({ data: { systemConfig: {} } })).toBeNull(); |
|||
expect(readVideoPosterQualityConfig({ systemConfig: { ossVideoSnapshotQuality: 0 } })).toBeNull(); |
|||
expect(readVideoPosterQualityConfig(null)).toBeNull(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,54 @@ |
|||
/** |
|||
* 视频封面(首帧)统一走地址拼接方案,不再依赖后端返回的 cover / coverThumb 字段。 |
|||
* |
|||
* 由视频真实地址现拼首帧封面 URL(腾讯云 CI snapshot 截帧 + imageMogr2 压缩)。 |
|||
* 例如: |
|||
* https://static.example/xx.mp4
|
|||
* -> https://static.example/xx.mp4?ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/80
|
|||
*/ |
|||
|
|||
/** imageMogr2 压缩质量默认值(1-100),可由后端 systemConfig.ossVideoSnapshotQuality 覆盖。 */ |
|||
export const DEFAULT_VIDEO_POSTER_QUALITY = 80; |
|||
|
|||
function isRecord(value: unknown): value is Record<string, unknown> { |
|||
return Boolean(value) && typeof value === "object" && !Array.isArray(value); |
|||
} |
|||
|
|||
/** 归一化压缩质量:接受 number 或数字字符串,限制在 [1, 100],非法返回 null。 */ |
|||
export function normalizeVideoPosterQuality(value: unknown): number | null { |
|||
const num = typeof value === "string" ? Number(value) : value; |
|||
if (typeof num !== "number" || !Number.isFinite(num)) return null; |
|||
const rounded = Math.round(num); |
|||
if (rounded < 1 || rounded > 100) return null; |
|||
return rounded; |
|||
} |
|||
|
|||
/** 从 /api/config 响应中读取视频封面压缩质量配置,未配置返回 null。 */ |
|||
export function readVideoPosterQualityConfig(configResponse: unknown): number | null { |
|||
if (!isRecord(configResponse)) return null; |
|||
const data = isRecord(configResponse.data) ? configResponse.data : configResponse; |
|||
const systemConfig = isRecord(data.systemConfig) ? data.systemConfig : null; |
|||
if (!systemConfig) return null; |
|||
return normalizeVideoPosterQuality(systemConfig.ossVideoSnapshotQuality); |
|||
} |
|||
|
|||
/** 仅 http(s) 地址可做 COS 截帧;blob:/data: 等本地地址不支持。 */ |
|||
export function isHttpVideoPosterCandidate(url: string | null | undefined): url is string { |
|||
return typeof url === "string" && /^https?:\/\//i.test(url); |
|||
} |
|||
|
|||
/** |
|||
* 由视频 URL 现拼首帧封面 URL。 |
|||
* 非 http(s) 地址(blob:/data:/空值)返回 null,由调用方回退到 <video> 原生首帧。 |
|||
*/ |
|||
export function buildVideoPosterUrl( |
|||
videoUrl: string | null | undefined, |
|||
quality: number = DEFAULT_VIDEO_POSTER_QUALITY |
|||
): string | null { |
|||
if (!isHttpVideoPosterCandidate(videoUrl)) return null; |
|||
|
|||
const normalizedQuality = normalizeVideoPosterQuality(quality) ?? DEFAULT_VIDEO_POSTER_QUALITY; |
|||
const params = `ci-process=snapshot&time=1&format=jpg&imageMogr2/format/webp/quality/${normalizedQuality}`; |
|||
const separator = videoUrl.includes("?") ? "&" : "?"; |
|||
return `${videoUrl}${separator}${params}`; |
|||
} |
|||
Loading…
Reference in new issue