提交 163b7c3d authored 作者: 王鹏飞's avatar 王鹏飞

chore: 修改视频上传

上级 f3c48f1e
...@@ -87,9 +87,13 @@ export function checkLocalFile(params: { file_name: string }) { ...@@ -87,9 +87,13 @@ export function checkLocalFile(params: { file_name: string }) {
} }
// 上传文件 // 上传文件
export function uploadLocalFile(data: { file: File; file_name: string; is_continuingly?: number; now_package_num: number; total_package_num: number }) { export function uploadLocalFile(
return httpRequest.post('/api/lab/v1/common/file/upload', data, { data: { file: File | Blob; file_name: string; is_continuingly?: number; now_package_num: number; total_package_num: number },
withCredentials: false, options = {}
headers: { 'Content-Type': 'multipart/form-data' } ) {
}) return httpRequest.post(
'/api/lab/v1/common/file/upload',
data,
Object.assign({ withCredentials: false, headers: { 'Content-Type': 'multipart/form-data' } }, options)
)
} }
<script setup lang="ts"> <script setup lang="ts">
import OSS from 'ali-oss'
import '@/lib/aliyun-upload-sdk/aliyun-upload-sdk-1.5.3.min.js'
import { VideoPlay } from '@element-plus/icons-vue' import { VideoPlay } from '@element-plus/icons-vue'
import { getUploadVideoAuth, updateUploadVideoAuth } from '@/api/base' import { useUpload } from '@/composables/useUpload'
;(window as any).OSS = OSS
// uploadInfo 包含要上传的文件信息
interface UploadInfo {
bucket: string
checkpoint: { file: File; name: string; fileSize: number; partSize: number; uploadId: string }
endpoint: string
file: File
fileHash: string
isImage: boolean
loaded: number
object: string
region: string
retry: boolean
ri: string
state: string
userData: string
videoId: string
videoInfo: any
progress: number
}
interface Props { interface Props {
beforeUpload?: (file: any) => void beforeUpload?: (file: any) => void
} }
...@@ -31,12 +10,16 @@ const props = defineProps<Props>() ...@@ -31,12 +10,16 @@ const props = defineProps<Props>()
const emit = defineEmits(['success']) const emit = defineEmits(['success'])
const uploader = createUploader() const { files, addFile, stopUpload } = useUpload({
let fileList = $ref<UploadInfo[]>([]) onUploadSucceed(file) {
emit('success', file)
}
})
onUnmounted(() => { onUnmounted(() => {
uploader && uploader.stopUpload() stopUpload()
}) })
const fileChange = async (event: Event) => { const fileChange = async (event: Event) => {
const element = event.currentTarget as HTMLInputElement const element = event.currentTarget as HTMLInputElement
const files: FileList | null = element.files const files: FileList | null = element.files
...@@ -44,104 +27,16 @@ const fileChange = async (event: Event) => { ...@@ -44,104 +27,16 @@ const fileChange = async (event: Event) => {
if (props.beforeUpload) { if (props.beforeUpload) {
try { try {
await props.beforeUpload(files) await props.beforeUpload(files)
uploader.cleanList()
} catch (error) { } catch (error) {
console.log(error) console.log(error)
return false return false
} }
} }
for (const file of files) { for (const file of files) {
// 是否重复上传 addFile(file)
const hasRepeat = !!fileList.find(
item =>
item.file.name === file.name && item.file.size === file.size && item.file.lastModified === file.lastModified
)
!hasRepeat && uploader.addFile(file, null, null, null, '{"Vod":{}}')
} }
uploader.startUpload()
fileList = uploader._uploadList
}
function updateFileList(uploadInfo: UploadInfo) {
if (!uploadInfo) return
fileList = fileList.map(item => {
if (item.ri === uploadInfo.ri) {
return { ...item, ...uploadInfo }
}
return item
})
}
function createUploader() {
return new (window as any).AliyunUpload.Vod({
//userID,必填,您可以使用阿里云账号访问账号中心(https://account.console.aliyun.com/),即可查看账号ID
userId: '1303984639806000',
//上传到视频点播的地域,默认值为'cn-shanghai',
//eu-central-1,ap-southeast-1
region: 'cn-shanghai',
//分片大小默认1 MB,不能小于100 KB(100*1024)
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: onUploadStarted,
//文件上传成功
onUploadSucceed: onUploadSucceed,
//文件上传失败
onUploadFailed: onUploadFailed,
//文件上传进度,单位:字节
onUploadProgress: onUploadProgress,
//上传凭证或STS token超时
onUploadTokenExpired: onUploadTokenExpired,
//全部文件上传结束
onUploadEnd: onUploadEnd
})
}
// 开始上传
function onUploadStarted(uploadInfo: UploadInfo) {
console.log('onUploadStarted', uploadInfo)
getUploadVideoAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name }).then(res => {
uploader.setUploadAuthAndAddress(uploadInfo, res.data.upload_auth, res.data.upload_address, res.data.source_id)
})
updateFileList(uploadInfo)
}
// 文件上传成功
function onUploadSucceed(uploadInfo: UploadInfo) {
console.log('onUploadSucceed', uploadInfo)
updateFileList(uploadInfo)
emit('success', uploadInfo)
}
//文件上传失败
function onUploadFailed(uploadInfo: UploadInfo, code: number, message: string) {
console.log('onUploadFailed', uploadInfo, code, message)
updateFileList(uploadInfo)
}
//文件上传进度,单位:字节
function onUploadProgress(uploadInfo: UploadInfo, totalSize: number, loadedPercent: number) {
console.log('onUploadProgress', uploadInfo.file.name, uploadInfo, totalSize, loadedPercent)
updateFileList(uploadInfo)
}
//上传凭证或STS token超时
function onUploadTokenExpired(uploadInfo: UploadInfo) {
console.log('onUploadTokenExpired', uploadInfo)
updateUploadVideoAuth({ source_id: uploadInfo.videoId }).then(res => {
uploader.resumeUploadWithAuth(res.data.UploadAuth)
})
updateFileList(uploadInfo)
}
// 全部文件上传结束
function onUploadEnd(uploadInfo: UploadInfo) {
console.log('onUploadEnd', uploadInfo)
updateFileList(uploadInfo)
}
// 进度条
function percentage(value: number) {
return parseFloat((value ? value * 100 : 0).toFixed(2))
} }
defineExpose({ uploader, fileList: $$(fileList) }) defineExpose({ files })
</script> </script>
<template> <template>
<div class="upload-video"> <div class="upload-video">
...@@ -151,13 +46,13 @@ defineExpose({ uploader, fileList: $$(fileList) }) ...@@ -151,13 +46,13 @@ defineExpose({ uploader, fileList: $$(fileList) })
</div> </div>
<div class="tips">推荐视频格式:帧率为25fps\输出码率为4M\输出格式为mp4,建议采用格式工厂等工具处理后上传。</div> <div class="tips">推荐视频格式:帧率为25fps\输出码率为4M\输出格式为mp4,建议采用格式工厂等工具处理后上传。</div>
<ul class="upload-video-list" v-for="(item, index) in fileList" :key="index"> <ul class="upload-video-list" v-for="(item, index) in files" :key="index">
<li> <li>
<div class="upload-video-list__item-info"> <div class="upload-video-list__item-info">
<el-icon><VideoPlay /></el-icon> <el-icon><VideoPlay /></el-icon>
<p>{{ item.file?.name }}</p> <p>{{ item.file?.name }}</p>
</div> </div>
<el-progress :stroke-width="2" :percentage="percentage(item.loaded)" v-if="item.loaded !== 1" /> <el-progress :stroke-width="2" :percentage="item.progress" v-if="item.progress !== 100" />
</li> </li>
</ul> </ul>
</div> </div>
......
import { getLocalFileChunk, uploadLocalFile } from '@/api/base'
interface FileItem {
file: File
url: string
name: string
progress: number
abortController: AbortController
}
interface UploadOptions {
multiple?: boolean
autoUpload?: boolean
onUploadStarted?: (file: FileItem) => void
onUploadSucceed?: (file: FileItem) => void
onUploadFailed?: (file: FileItem, error: any) => void
onUploadProgress?: (file: FileItem, progress: number) => void
onUploadEnd?: () => void
}
export function useUpload(options: UploadOptions = {}) {
options = Object.assign({ autoUpload: true, multiple: false }, options)
const files = ref<FileItem[]>([])
const uploading = ref(false)
function addFile(file: File) {
// 检查文件是否已经在队列中
const existingFileIndex = files.value.findIndex(f => f.name === file.name)
// 如果文件已经在队列中,并且上传已经开始,则直接返回
if (existingFileIndex !== -1 && files.value[existingFileIndex].progress > 0) return
if (!options.multiple) stopUpload()
const abortController = new AbortController()
files.value.push({ url: '', name: file.name, file, progress: 0, abortController })
if (options.autoUpload && !uploading.value) startUpload()
}
async function startUpload() {
uploading.value = true
for (const file of files.value) {
if (file.progress === 0) {
// 只处理尚未开始上传的文件
try {
options?.onUploadStarted?.(file)
await uploadFile(file)
options?.onUploadSucceed?.(file)
} catch (error) {
options?.onUploadFailed?.(file, error)
}
}
}
uploading.value = false
options?.onUploadEnd?.()
}
async function uploadFile(file: FileItem) {
const {
data: { detail }
} = await getLocalFileChunk({ file_name: file.file.name, file_size: file.file.size })
const fileName = detail.file_name
const chunkSize = detail.chunk_size
const totalChunks = Math.ceil(file.file.size / chunkSize)
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = file.file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize)
await uploadChunk({ file, chunk, chunkIndex, fileName, totalChunks })
}
}
async function uploadChunk({
file,
chunk,
chunkIndex,
fileName,
totalChunks
}: {
file: FileItem
chunk: Blob
chunkIndex: number
fileName: string
totalChunks: number
}) {
const {
data: { detail }
} = await uploadLocalFile(
{ file_name: fileName, file: chunk, now_package_num: chunkIndex + 1, total_package_num: totalChunks },
{
signal: file.abortController.signal,
onUploadProgress(event: ProgressEvent) {
updateProgress({ file, event, chunkIndex, totalChunks })
}
}
)
file.url = detail.uri
}
function updateProgress({ event, chunkIndex, totalChunks, file }: { file: FileItem; event: ProgressEvent; chunkIndex: number; totalChunks: number }) {
const totalSize = event.total * totalChunks
const loadedSize = event.loaded + chunkIndex * event.total
const progressPercent = (loadedSize / totalSize) * 100
file.progress = parseFloat(progressPercent.toFixed(2))
options?.onUploadProgress?.(file, file.progress)
console.log(file.progress)
}
function cancelUpload(file: FileItem) {
file.abortController.abort()
files.value = files.value.filter(item => item.name !== file.name)
}
function stopUpload() {
files.value.forEach(cancelUpload) // 停止所有文件的上传
files.value.length = 0
uploading.value = false // 更新上传状态
}
return { files, uploading, addFile, startUpload, cancelUpload, stopUpload }
}
...@@ -29,7 +29,8 @@ const form = reactive<any>({ ...@@ -29,7 +29,8 @@ const form = reactive<any>({
source_id: '', source_id: '',
duration: 0, duration: 0,
type: '', type: '',
protocol: false protocol: false,
size: ''
}) })
watchEffect(() => { watchEffect(() => {
if (!props.data) return if (!props.data) return
...@@ -69,7 +70,8 @@ function handleBeforeUpload() { ...@@ -69,7 +70,8 @@ function handleBeforeUpload() {
function handleUploadSuccess(uploadInfo: any) { function handleUploadSuccess(uploadInfo: any) {
form.name = uploadInfo.file.name.split('.').shift() form.name = uploadInfo.file.name.split('.').shift()
form.source_id = uploadInfo.videoId form.source_id = uploadInfo.url
form.size = uploadInfo.file.size
} }
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
......
...@@ -6,21 +6,25 @@ interface Props { ...@@ -6,21 +6,25 @@ interface Props {
data: ContestVideoItem data: ContestVideoItem
} }
const props = defineProps<Props>() const props = defineProps<Props>()
let playList = $ref<any>([]) let playList = $ref<any>()
function fetchInfo() { function fetchInfo() {
getContestVideoPalyInfo({ source_id: props.data.source_id }).then(res => { getContestVideoPalyInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.replay_list.play_info_list playList = res.data.replay_list.play_info_list
}) })
} }
const playUrl = $computed(() => { const playUrl = $computed(() => {
const definitionUrl = playList.reduce( if (Array.isArray(playList)) {
(result: any, item: any) => { const definitionUrl = playList.reduce(
result[item.Definition] = item.PlayURL (result: any, item: any) => {
return result result[item.Definition] = item.PlayURL
}, return result
{ SD: '', LD: '', FD: '' } },
) { SD: '', LD: '', FD: '' }
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD )
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD
} else {
return playList
}
}) })
onMounted(() => { onMounted(() => {
fetchInfo() fetchInfo()
...@@ -29,8 +33,6 @@ onMounted(() => { ...@@ -29,8 +33,6 @@ onMounted(() => {
<template> <template>
<el-dialog title="查阅操作视频"> <el-dialog title="查阅操作视频">
<AppVideoPlayer <AppVideoPlayer :options="{ fluid: true, sources: [{ src: playUrl }] }" v-if="playUrl"></AppVideoPlayer>
:options="{ fluid: true, sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
v-if="playUrl"></AppVideoPlayer>
</el-dialog> </el-dialog>
</template> </template>
...@@ -109,6 +109,7 @@ export interface ContestVideoCreateParams { ...@@ -109,6 +109,7 @@ export interface ContestVideoCreateParams {
status: string status: string
duration: number duration: number
source_id: string source_id: string
size: string
} }
export type ContestVideoUpdateParams = ContestVideoCreateParams & { id: string } export type ContestVideoUpdateParams = ContestVideoCreateParams & { id: string }
...@@ -33,7 +33,8 @@ const form = reactive<VideoCreateItem>({ ...@@ -33,7 +33,8 @@ const form = reactive<VideoCreateItem>({
experiment_id: '', experiment_id: '',
status: '1', status: '1',
source_id: '', source_id: '',
protocol: false protocol: false,
size: ''
}) })
watchEffect(() => { watchEffect(() => {
if (!props.data) return if (!props.data) return
...@@ -75,7 +76,8 @@ function handleBeforeUpload() { ...@@ -75,7 +76,8 @@ function handleBeforeUpload() {
function handleUploadSuccess(uploadInfo: any) { function handleUploadSuccess(uploadInfo: any) {
form.name = uploadInfo.file.name.split('.').shift() form.name = uploadInfo.file.name.split('.').shift()
form.source_id = uploadInfo.videoId form.source_id = uploadInfo.url
form.size = uploadInfo.file.size
} }
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
......
...@@ -29,4 +29,5 @@ export interface VideoCreateItem { ...@@ -29,4 +29,5 @@ export interface VideoCreateItem {
name: string name: string
source_id: string source_id: string
protocol: boolean protocol: boolean
size: string
} }
...@@ -9,7 +9,7 @@ interface Props { ...@@ -9,7 +9,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
let detail = $ref<VideoItem | null>(null) let detail = $ref<VideoItem | null>(null)
let playList = $ref<any>([]) let playList = $ref<any>()
function fetchInfo() { function fetchInfo() {
if (!props.id) return if (!props.id) return
...@@ -19,14 +19,18 @@ function fetchInfo() { ...@@ -19,14 +19,18 @@ function fetchInfo() {
}) })
} }
const playUrl = $computed(() => { const playUrl = $computed(() => {
const definitionUrl = playList.reduce( if (Array.isArray(playList)) {
(result: any, item: any) => { const definitionUrl = playList.reduce(
result[item.Definition] = item.PlayURL (result: any, item: any) => {
return result result[item.Definition] = item.PlayURL
}, return result
{ SD: '', LD: '', FD: '' } },
) { SD: '', LD: '', FD: '' }
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD )
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD
} else {
return playList
}
}) })
onMounted(() => { onMounted(() => {
fetchInfo() fetchInfo()
...@@ -47,9 +51,7 @@ onMounted(() => { ...@@ -47,9 +51,7 @@ onMounted(() => {
<el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item> <el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div style="height: 600px" v-if="playUrl"> <div style="height: 600px" v-if="playUrl">
<AppVideoPlayer <AppVideoPlayer :options="{ sources: [{ src: playUrl }] }" style="width: 100%; height: 100%"></AppVideoPlayer>
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 100%"></AppVideoPlayer>
</div> </div>
</template> </template>
</AppCard> </AppCard>
......
<script setup lang="ts"> <script setup lang="ts">
import type { ExperimentVideoType, PlayInfo } from '../types' import type { ExperimentVideoType } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api' import { getExperimentVideoPlayInfo } from '../api'
...@@ -11,7 +11,7 @@ interface Props { ...@@ -11,7 +11,7 @@ interface Props {
data: ExperimentVideoType data: ExperimentVideoType
} }
const props = defineProps<Props>() const props = defineProps<Props>()
let playList = $ref<PlayInfo[]>([]) let playList = $ref<any>()
function fetchInfo() { function fetchInfo() {
getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => { getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.list.play_info_list playList = res.data.list.play_info_list
...@@ -28,14 +28,18 @@ function fetchInfo() { ...@@ -28,14 +28,18 @@ function fetchInfo() {
}) })
} }
const playUrl = $computed(() => { const playUrl = $computed(() => {
const definitionUrl = playList.reduce( if (Array.isArray(playList)) {
(result: any, item: any) => { const definitionUrl = playList.reduce(
result[item.Definition] = item.PlayURL (result: any, item: any) => {
return result result[item.Definition] = item.PlayURL
}, return result
{ SD: '', LD: '', FD: '' } },
) { SD: '', LD: '', FD: '' }
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD )
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD
} else {
return playList
}
}) })
onMounted(() => { onMounted(() => {
fetchInfo() fetchInfo()
...@@ -46,9 +50,7 @@ onMounted(() => { ...@@ -46,9 +50,7 @@ onMounted(() => {
<h2>{{ data.name }}</h2> <h2>{{ data.name }}</h2>
<!-- <img :src="data.cover" /> --> <!-- <img :src="data.cover" /> -->
<div style="height: 200px" v-if="playUrl"> <div style="height: 200px" v-if="playUrl">
<AppVideoPlayer <AppVideoPlayer :options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }" style="width: 100%; height: 100%"></AppVideoPlayer>
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 100%"></AppVideoPlayer>
</div> </div>
</div> </div>
</template> </template>
......
<script setup lang="ts"> <script setup lang="ts">
import type { ExperimentVideoType, PlayInfo } from '../types' import type { ExperimentVideoType, } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api' import { getExperimentVideoPlayInfo } from '../api'
...@@ -12,7 +12,7 @@ interface Props { ...@@ -12,7 +12,7 @@ interface Props {
data: ExperimentVideoType data: ExperimentVideoType
} }
const props = defineProps<Props>() const props = defineProps<Props>()
let playList = $ref<PlayInfo[]>([]) let playList = $ref<any>()
function fetchInfo() { function fetchInfo() {
getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => { getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.play_info_list playList = res.data.play_info_list
...@@ -29,14 +29,18 @@ function fetchInfo() { ...@@ -29,14 +29,18 @@ function fetchInfo() {
}) })
} }
const playUrl = $computed(() => { const playUrl = $computed(() => {
const definitionUrl = playList.reduce( if (Array.isArray(playList)) {
(result: any, item: any) => { const definitionUrl = playList.reduce(
result[item.Definition] = item.PlayURL (result: any, item: any) => {
return result result[item.Definition] = item.PlayURL
}, return result
{ SD: '', LD: '', FD: '' } },
) { SD: '', LD: '', FD: '' }
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD )
return definitionUrl.SD || definitionUrl.LD || definitionUrl.FD
} else {
return playList
}
}) })
onMounted(() => { onMounted(() => {
fetchInfo() fetchInfo()
...@@ -47,9 +51,7 @@ onMounted(() => { ...@@ -47,9 +51,7 @@ onMounted(() => {
<h2>{{ data.name }}</h2> <h2>{{ data.name }}</h2>
<!-- <img :src="data.cover" /> --> <!-- <img :src="data.cover" /> -->
<div style="height: 200px" v-if="playUrl"> <div style="height: 200px" v-if="playUrl">
<AppVideoPlayer <AppVideoPlayer :options="{ sources: [{ src: playUrl }] }" style="width: 100%; height: 100%"></AppVideoPlayer>
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 100%"></AppVideoPlayer>
</div> </div>
</div> </div>
</template> </template>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论