mrr.sj.front/components/upload-video/upload-video.vue

421 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 上传短视频 -->
<view class="upload-content">
<view class="progress-box" v-if="showProgress">
<text class="progress-text">{{progress}}%</text>
<text class="progress-lable">上传中</text>
<progress :percent="progress"></progress>
</view>
<view v-if="video" class="video-preview">
<video
:src="'https://app.mrrweb.com.cn' + video"
ref="videoPlayer"
v-if="video"
class="preview-video"
:controls="false"
object-fit="cover"
:autoplay="true"
:muted="true"
:initial-time="1"
:show-center-play-btn="false"
:show-loading="false"
:enable-progress-gesture="false"
:show-fullscreen-btn="false"
:show-play-btn="false"
@play="onPlay"
></video>
<!-- 备用提示 -->
<view v-if="showFallback" class="video-fallback">
<text>视频加载失败,请尝试刷新或更换视频源</text>
</view>
<view class="delete-btn" @click="deleteVideo">×</view>
</view>
<view v-else class="upload-btn" @click="chooseVideo">
<text class="plus-icon">+</text>
</view>
</view>
</template>
<script>
import request from '../../utils/request';
import sparkMD5 from 'spark-md5'; // 引入MD5计算库
export default {
name:"upload-video",
props: {
video: {
type: String,
default: ''
},
videoNum: {
type: Number,
default: 1
},
},
data() {
return {
autoplay: false,
showProgress: false,
progress: 0,
showFallback: false,
finalVideoUrl: '',
platformAutoplay: true,
autoplayMuted: true,
};
},
// watch: {
// video: {
// handler(newValue) {
// this.video = newValue
// },
// deep: true
// }
// },
methods: {
// 获取文件 (兼容各平台)
getFile() {
return new Promise((resolve, reject) => {
// #ifdef H5
const input = document.createElement('input')
input.type = 'file'
input.accept = this.accept
input.onchange = (e) => {
// resolve(e.target.files[0])
resolve(e)
}
input.click()
// #endif
// #ifndef H5
uni.chooseVideo({
count: 1,
sourceType: ['album', 'camera'],
compressed: false,
maxDuration: 180,
camera: 'back',
success: (res) => {
// resolve(res.tempFiles[0])
resolve(res)
},
fail: (err) => {
reject(err)
}
})
// #endif
})
},
// 上传视频区域
async chooseVideo() {
// 选择文件(添加类型校验)
// const res = await uni.chooseVideo({
// sourceType: ['album', 'camera'],
// compressed: false,
// maxDuration: 180,
// camera: 'back'
// });
const res = await this.getFile();
console.log(res,';;;;;;;;;;;;;;;;;;;;;;;;;;;')
this.showProgress = true;
let size = res.size;
if(size > 5*1024*1024) {
// 统一获取文件列表
const files = res.tempFile || res;
const file = res.tempFile || res;
console.log('file',file)
// if (!files || files.length === 0) {
// uni.showToast({ title: '文件选择取消', icon: 'none' });
// return;
// }
if (!file) {
uni.showToast({ title: '视频选择取消', icon: 'none' });
return;
}
progress: (res) => {
console.log(`分片${index}进度: ${res.progress}%`);
}
// 通过 tempFiles 获取文件UniApp规范
// const file = files[0];
// 计算文件MD5优化大文件计算
const md5 = await this.calculateFileMD5(file);
// 分片大小2MB
const chunkSize = 1 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
// 分片上传
for (let index = 0; index < totalChunks; index++) {
const start = index * chunkSize;
const end = Math.min(file.size, start + chunkSize);
// #ifdef H5
// 浏览器环境使用标准的 slice 方法
const chunk = file.slice(start, end);
// 上传分片
await this.uploadChunk(chunk, index, totalChunks, md5);
// #endif
// #ifndef H5
// 非浏览器环境使用 Uniapp API
const fs = uni.getFileSystemManager();
const arrayBuffer = fs.readFileSync({
filePath: file,
position: start,
length: end - start
});
// 上传分片
await this.uploadChunk(arrayBuffer, index, totalChunks, md5);
// #endif
// 上传分片
// await this.uploadChunk(chunk, index, totalChunks, md5);
this.progress = parseInt((index+1)/totalChunks*100)
}
// 合并请求
await this.mergeChunks(md5, file.name);
}else {
let item = {
filePath: res.tempFilePath,
type: 'video'
}
this.uploadVideo(item)
}
},
uploadVideo(item) {
request.uploadVideo('/user/uploadvideo',item.filePath,'video',{type: item.type}).then(res=>{
this.$emit('addVideo',res.path)
// this.initVideo();
this.showProgress = false;
})
},
// 计算文件MD5分块计算
calculateFileMD5(file) {
return new Promise((resolve) => {
const chunkSize = 1 * 1024 * 1024;
const chunks = Math.ceil(file.size / chunkSize);
const spark = new sparkMD5.ArrayBuffer();
let currentChunk = 0;
const loadNext = () => {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
// #ifdef H5
const reader = new FileReader();
// #endif
// #ifndef H5
const reader = uni.getfilesystem;
// #endif
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
reader.readAsArrayBuffer(file.slice(start, end));
};
loadNext();
});
},
async uploadChunk(chunk, index, totalChunks, md5) {
let that = this;
try {
// 获取ArrayBuffer
const arrayBuffer = await new Promise((resolve) => {
// #ifdef H5
const reader = new FileReader();
// #endif
// #ifndef H5
const reader = uni.getfilesystem;
// #endif
reader.onload = () => resolve(reader.result);
reader.readAsArrayBuffer(chunk);
});
// 平台差异化处理
let tempFilePath = '';
// 微信小程序/App端写入临时文件
// #ifdef MP-WEIXIN || APP-PLUS
const fs = uni.getFileSystemManager();
tempFilePath = `${getTempDir()}${md5}_${Date.now()}_${index}.tmp`;
await new Promise((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
data: arrayBuffer,
encoding: 'binary',
success: resolve,
fail: (err) => reject(`文件写入失败: ${JSON.stringify(err)}`)
});
});
// #endif
// H5端特殊处理
// #ifdef H5
const blob = new Blob([arrayBuffer], { type: chunk.type });
const file = new File([blob], `${md5}_${index}.bin`, {
type: chunk.type,
lastModified: Date.now()
});
tempFilePath = file;
// #endif
const uploadTask = await request.uploadTask('/user/uploadvideoother',tempFilePath,'file',{chunkIndex: index,totalChunks: totalChunks,md5: md5});
// 上传后清理临时文件非H5
// #ifdef MP-WEIXIN || APP-PLUS
fs.unlink({ filePath: tempFilePath });
// #endif
return uploadTask;
} catch (error) {
console.error('分片上传失败:', error);
throw new Error(`分片${index}上传失败: ${error.message}`);
}
},
// 合并请求
mergeChunks(md5, fileName) {
let that = this;
request.post('/user/uploadvideomerge',{md5: md5,filename: fileName}).then(res=>{
that.$emit('addVideo',res.path)
// that.initVideo();
that.showProgress = false;
})
},
// 上传视频结束区域
// 删除视频
deleteVideo() {
this.$emit('deleteVideo')
},
initVideo() {
// 平台自动播放处理
this.platformAutoplay = this.autoplay;
// iOS自动播放必须静音
// #ifdef APP-PLUS
if (plus.os.name === 'iOS' && this.autoplay) {
this.autoplayMuted = true;
}
// #endif
// 路径处理
this.processVideoUrl();
},
processVideoUrl() {
if (!this.video) return;
// 处理APP本地路径
if (this.video.startsWith('file://') || this.video.startsWith('/storage')) {
// #ifdef APP-PLUS
this.finalVideoUrl = plus.io.convertLocalFileSystemURL(this.video);
// #endif
// #ifdef MP-WEIXIN || H5
this.finalVideoUrl = this.video;
// #endif
} else {
this.finalVideoUrl = this.video;
}
},
onPlay() {
console.log('视频开始播放');
this.platformAutoplay = true;
this.showFallback = false;
},
onError(e) {
console.error('视频播放错误:', e);
this.showFallback = true;
// 尝试备用方案
this.tryFallbackSource();
},
tryFallbackSource() {
// 可以在这里实现备用视频源逻辑
}
}
}
</script>
<style>
.upload-content {
display: flex;
flex-wrap: wrap;
position: relative;
}
.progress-box {
width: 204rpx;
height: 204rpx;
background-color: rgba(0 ,0 ,0 , 0.5);
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 99;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
.progress-text {
font-size: 42rpx;
color: #FFFFFF;
font-weight: 500;
}
.progress-lable {
font-size: 28rpx;
color: #FFFFFF;
}
.progress-box progress {
/* width: 204rpx; */
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.video-preview,
.upload-btn {
width: 200rpx;
height: 200rpx;
margin-right: 10rpx;
margin-bottom: 20rpx;
position: relative;
}
.preview-video {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
}
.upload-btn {
border: 2rpx dashed #DDDDDD;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
}
.plus-icon {
font-size: 60rpx;
color: #999999;
}
.delete-btn {
position: absolute;
top: -20rpx;
right: -20rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
color: #FFFFFF;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
}
</style>