Commit 534f21d2 by 王立鹏

Merge branch '线上优化/码率优化' into 'master'

线上优化/码率优化

See merge request !37
parents 4389af5b 207f6eea
* text=auto eol=lf
# 二进制文件不做换行符转换 否则安装包会被改坏
*.exe binary
......@@ -41,6 +41,7 @@
"element-plus": "^2.11.5",
"gsap": "^3.14.2",
"inquirer": "^13.0.2",
"mediainfo.js": "^0.3.7",
"notivue": "^2.4.5",
"pinia": "^3.0.3",
"ssh2": "^1.17.0",
......
......@@ -53,6 +53,9 @@ importers:
inquirer:
specifier: ^13.0.2
version: 13.0.2(@types/node@22.18.12)
mediainfo.js:
specifier: ^0.3.7
version: 0.3.7
notivue:
specifier: ^2.4.5
version: 2.4.5(defu@6.1.4)
......@@ -2118,6 +2121,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
cliui@9.0.1:
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
engines: {node: '>=20'}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
......@@ -2742,6 +2749,10 @@ packages:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
......@@ -3398,6 +3409,11 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
mediainfo.js@0.3.7:
resolution: {integrity: sha512-mgsmb2GrCTAguVcTohW7KrF+QXNaihrbljj75pLWDQcTTQyqfGoZG4Dy++AZ+FDVCT14LHvVGC9SXxcF5VePag==}
engines: {node: '>=18.0.0'}
hasBin: true
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
......@@ -4232,6 +4248,10 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.2.2:
resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
engines: {node: '>=20'}
string.prototype.trim@1.2.10:
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
engines: {node: '>= 0.4'}
......@@ -4851,10 +4871,18 @@ packages:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs-parser@22.0.0:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@18.1.0:
resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
......@@ -6909,6 +6937,12 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
cliui@9.0.1:
dependencies:
string-width: 7.2.0
strip-ansi: 7.1.2
wrap-ansi: 9.0.2
clone@2.1.2: {}
codepage@1.15.0: {}
......@@ -7604,6 +7638,8 @@ snapshots:
get-east-asian-width@1.4.0: {}
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
......@@ -8199,6 +8235,10 @@ snapshots:
mdn-data@2.27.1: {}
mediainfo.js@0.3.7:
dependencies:
yargs: 18.1.0
memoize-one@6.0.0: {}
memorystream@0.3.1: {}
......@@ -9104,6 +9144,11 @@ snapshots:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.2
string-width@8.2.2:
dependencies:
get-east-asian-width: 1.6.0
strip-ansi: 7.1.2
string.prototype.trim@1.2.10:
dependencies:
call-bind: 1.0.8
......@@ -9797,6 +9842,8 @@ snapshots:
yargs-parser@21.1.1: {}
yargs-parser@22.0.0: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
......@@ -9807,6 +9854,15 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@18.1.0:
dependencies:
cliui: 9.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
string-width: 8.2.2
y18n: 5.0.8
yargs-parser: 22.0.0
yocto-queue@0.1.0: {}
zip-stream@6.0.1:
......
This diff was suppressed by a .gitattributes entry.
......@@ -12,7 +12,7 @@
accept="video/*"
class="upload-area"
>
<div v-if="!uploading && !videoInfo" class="upload-placeholder">
<div v-if="!uploading && !videoInfo && !codecChecking" class="upload-placeholder">
<el-icon class="upload-icon"><IEpVideoCamera /></el-icon>
<div class="upload-text">
<p>拖拽视频文件到此处,或点击选择文件</p>
......@@ -20,6 +20,17 @@
</div>
</div>
<!-- 上传前的视频编码检测 -->
<div v-if="codecChecking" class="upload-progress">
<div class="progress-info">
<el-icon class="progress-icon is-loading"><IEpLoading /></el-icon>
<div class="progress-details">
<p class="file-name">{{ currentFile?.name }}</p>
<p class="progress-text">正在检测视频编码格式...</p>
</div>
</div>
</div>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<div class="progress-info">
......@@ -71,13 +82,18 @@
</div>
</template>
<script setup lang="ts">
<script setup lang="tsx">
import { uploadFile as uploadFileApi } from '@/api/common'
import type { UploadFile } from 'element-plus'
import type { UploadVideoProps } from './types'
import { getVideoMetadata } from '@/utils'
import { useMediaInfo, useMessageBox } from '@/hooks'
import { push } from 'notivue'
// 视频转码工具下载地址 安装包放在 public/downloads 下
// 测试环境打包带了 base 前缀 所以要拼 BASE_URL 不能写死根路径
const H265_TOOL_DOWNLOAD_URL = `${import.meta.env.BASE_URL}downloads/YswgToolbox_0.3.0_x64-setup.exe`
interface VideoInfo {
url: string
name: string
......@@ -92,6 +108,7 @@ const {
maxSize = 1000,
acceptFormats = ['mp4', 'avi', 'mov', 'wmv', 'flv'],
btnClass = '',
checkVideoCodec = true,
} = defineProps<UploadVideoProps>()
const modelValue = defineModel<string>('modelValue', { required: true })
const videoInfo = defineModel<VideoInfo | null>('videoInfo', { required: false, default: null })
......@@ -106,6 +123,12 @@ const uploading = ref(false)
const uploadProgress = ref(0)
const uploadError = ref('')
const currentFile = ref<File | null>(null)
// 正在检测视频编码格式
const codecChecking = ref(false)
const { isH265Video, cleanup: cleanupMediaInfo } = useMediaInfo()
const { confirm } = useMessageBox()
onBeforeUnmount(cleanupMediaInfo)
// 文件大小格式化
const formatFileSize = (bytes: number): string => {
......@@ -138,12 +161,67 @@ const beforeUpload = (file: File): boolean => {
return true
}
/**
* 上传前检测视频编码格式
* H.265(HEVC) 编码的视频浏览器普遍无法解码播放 上传上去也是放不了的
* 所以先提示用户去转成 H.264 再传 用户坚持的话也允许继续上传
* 返回 true 表示继续上传
*/
const confirmVideoCodec = async (file: File) => {
let isH265 = false
try {
codecChecking.value = true
isH265 = await isH265Video(file)
} catch (error) {
// 检测失败(如 wasm 加载不到)不阻塞上传 按原流程走
console.error('视频编码检测失败', error)
} finally {
codecChecking.value = false
}
if (!isH265) return true
try {
await confirm({
title: '视频编码格式提示',
type: 'warning',
confirmText: '仍然上传',
cancelText: '取消上传',
message: () => (
<span>
<span class="block mb-2">检测到该视频为 H.265(HEVC) 编码,部分浏览器可能无法正常播放。</span>
<span class="block mb-2">建议使用转码工具将视频转换为 H.264 编码后再重新上传。</span>
<a
href={H265_TOOL_DOWNLOAD_URL}
target="_blank"
rel="noopener"
class="text-[#7083FF] underline"
>
点击下载视频转码工具
</a>
</span>
),
})
return true
} catch {
return false
}
}
// 文件选择处理 - 选择后自动上传
const handleFileChange = async (file: UploadFile) => {
if (!file.raw) return
currentFile.value = file.raw
// H.265 检测不通过且用户选择取消时 不进入上传
if (checkVideoCodec && !(await confirmVideoCodec(file.raw))) {
currentFile.value = null
// 清掉内部文件列表 否则再次选择同一个文件不会触发 change
uploadRef.value?.clearFiles()
return
}
await startUpload()
}
......@@ -272,6 +350,7 @@ const retryUpload = () => {
const reset = () => {
uploadRef.value = null
uploading.value = false
codecChecking.value = false
uploadProgress.value = 0
uploadError.value = ''
currentFile.value = null
......
......@@ -2,4 +2,6 @@ export interface UploadVideoProps {
maxSize?: number // MB
acceptFormats?: string[]
btnClass?: string
/** 上传前是否检测视频编码 检测到 H.265(HEVC) 时提示用户先转成 H.264 默认开启 */
checkVideoCodec?: boolean
}
......@@ -5,3 +5,4 @@ export * from './useHintAnimation'
export * from './useUploadImg'
export * from './useNavigation'
export * from './useMessageBox.tsx'
export * from './useMediaInfo'
import mediaInfoFactory, { type MediaInfo } from 'mediainfo.js'
// mediainfo.js 的 wasm 体积较大 走 CDN 加载 避免打包进主包
const MEDIAINFO_VERSION = '0.3.7'
const MEDIAINFO_CDN_BASE = `https://cdn.jsdelivr.net/npm/mediainfo.js@${MEDIAINFO_VERSION}/dist/`
export const useMediaInfo = () => {
let mediainfoPromise: Promise<MediaInfo<'object'>> | null = null
function getMediaInfo() {
if (!mediainfoPromise) {
mediainfoPromise = mediaInfoFactory({
format: 'object',
locateFile: (path) => `${MEDIAINFO_CDN_BASE}${path}`,
})
}
return mediainfoPromise
}
/**
* 读取视频轨道的编码格式 如 AVC(H264)、HEVC(H265)
* 只读取文件的必要片段 不会把整个文件读进内存
*/
async function getVideoCodec(file: File) {
const mediainfo = await getMediaInfo()
const getSize = () => file.size
const readChunk = (size: number, offset: number) =>
file
.slice(offset, offset + size)
.arrayBuffer()
.then((buffer) => new Uint8Array(buffer))
const result = await mediainfo.analyzeData(getSize, readChunk)
const tracks = result?.media?.track || []
const videoTrack = tracks.find((track) => track['@type'] === 'Video')
return (videoTrack as { Format?: string } | undefined)?.Format || ''
}
/**
* 判断是否为 H.265(HEVC) 编码 这类视频浏览器普遍无法直接播放
*/
async function isH265Video(file: File) {
const codec = await getVideoCodec(file)
return /HEVC|H\.?265/i.test(codec)
}
// 释放 wasm 实例 组件卸载时调用
function cleanup() {
mediainfoPromise?.then((instance) => instance.close())
mediainfoPromise = null
}
return {
getVideoCodec,
isH265Video,
cleanup,
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment