布局及实现代码:
html
<template>
<view class="flex flex-column p-4 grid-gap-4">
<view class="flex flex-column grid-gap-4 bg-white p-4 rounded-4">
<view class="font-weight-600">视频名称</view>
<input type="text" v-model="form.name" class="bg-placeholder rounded-4 p-4" placeholder="请输入视频名称..."
maxlength="50">
</view>
<view class="flex flex-column grid-gap-4 bg-white p-4 rounded-4">
<view class="bg-placeholder rounded-4 p-4 flex flex-center" @tap="chooseVideo">
<video :src="tempFilePath" class="rounded-4" style="width: 100%;height: calc(200px * 100vw / 375px);"
:direction="0" v-if="form.video"></video>
<view class="flex flex-column grid-gap-2 flex-center py-10 text-center" v-else-if="!showVideo">
<view v-if="uploadState" class="flex flex-column w-100 grid-gap-2">
<view class="bg-grey w-100 rounded-round" style="height: 10px;">
<view class="bg-success rounded-round" style="height: 10px;transition: width .3s;"
:style="{ width: progress + '%' }"></view>
</view>
<text class="text-grey h6 text-center">{{ progress }}%</text>
</view>
<image src="@/static/uploads-plus.png" mode="scaleToFill" style="width: 50px;height: 50px;" v-else />
<text class="text-text">上传视频</text>
<!-- #ifdef MP-WEIXIN -->
<text class="text-grey h10">或者</text>
<view class="flex flex-center" @tap.stop="chooseMessageFile">
<text class="text-success h10">从微信中选择</text>
<uni-icons type="right" color="var(--xl-success)" size="12"></uni-icons>
</view>
<!-- #endif -->
<text class="text-grey h10">上传单人视频(建议用不说话视频)</text>
<text class="text-grey h10">注意每一帧都要有露脸,侧脸幅度不可过大</text>
</view>
<video :src="tempFilePath" class="rounded-4" style="width: 100%;height: calc(200px * 100vw / 375px);"
:direction="0" v-else-if="tempFilePath && showVideo" @loadedmetadata="loadedmetadata"></video>
</view>
<view class="font-weight-600">视频要求:</view>
<view class="grid-columns-4 grid-gap-4">
<view class="grid-column-2 flex">
<text class="text-grey h10">视频方向:</text>
<text class="text-text h10">横向或纵向</text>
</view>
<view class="grid-column-2 flex">
<text class="text-grey h10">分辨率:</text>
<text class="text-text h10">360p~4K</text>
</view>
<view class="grid-column-2 flex">
<text class="text-grey h10">文件格式:</text>
<text class="text-text h10">mp4,mov</text>
</view>
<view class="grid-column-2 flex">
<text class="text-grey h10">文件大小:</text>
<text class="text-text h10">不超过300M</text>
</view>
<view class="grid-column-2 flex">
<text class="text-grey h10">视频时长:</text>
<text class="text-text h10">10秒~5分钟</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue';
import { $message, $http, $page } from "@/utils";
import { onLoad, onShow } from "@dcloudio/uni-app";
const form = ref({
name: '',
video: ''
});
const tempFilePath = ref('');
let fileObj = {
name: '',
type: '',
size: ''
}
const loadedmetadata = (e: any) => {
showVideo.value = false;
console.log('选择的文件:', e);
if (e.detail?.duration < 5) {
uni.showModal({
title: '提示',
content: '视频时长不得少于5秒',
showCancel: false,
confirmText: '我知道了'
});
form.value.video = '';
tempFilePath.value = '';
} else if (!form.value.video) {
uploadFile();
}
}
const submit = () => {
if (!form.value.name) {
$message.error('请输入数字人分身名称');
return;
}
console.log('form:', form)
if (!form.value.video) {
$message.error('请上传视频');
return;
console.log('数据:', form.value);
}
}
const progress = ref(0);
const uploadState = ref(false);
const showVideo = ref(false);
// 其他端选择视频
const chooseVideo = () => {
uni.chooseVideo({
compressed: true, // WEBCONFIG.value?.upload_video_compressed ? true : false,
camera: 'front',
success: (res: any) => {
console.log('success:', res)
// 保存文件信息
fileObj = res.tempFile
tempFilePath.value = res.tempFilePath;
showVideo.value = true;
form.value.video = '';
// APP不支持@loadedmetadata
// #ifdef APP-PLUS
if (showVideo.value && tempFilePath.value) {
const val = {
detail: {
duration: res.duration
}
}
loadedmetadata(val)
}
// #endif
// form.value.video = res.tempFilePath;
}
})
}
// 文件上传
const uploadFile = () => {
uni.showLoading({
title: '上传中...',
mask: true
})
progress.value = 0;
uploadState.value = true;
// 微信小程序上传视频
const task = $http.upload('Generation/uploadVideo', {
filePath: tempFilePath.value,
name: 'file',
success: (res: any) => {
uni.hideLoading();
uploadState.value = false;
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
if (data.code === $http.ResponseCode.SUCCESS) {
form.value.video = data.data.url;
nextTick(() => {
showVideo.value = true;
})
} else {
$message.error(data.msg);
}
return;
} catch (e) {
}
}
$message.error('上传失败:' + JSON.stringify(res));
},
fail: (err: any) => {
uni.hideLoading();
uploadState.value = false;
$message.error('上传失败:' + JSON.stringify(err));
},
}, true);
task.onProgressUpdate((res: any) => {
progress.value = res.progress;
console.log('上传进度' + res.progress);
console.log('已经上传的数据长度' + res.totalBytesSent);
console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
});
}
// 微信小程序选择视频
const chooseMessageFile = () => {
wx.chooseMessageFile({
count: 1,
type: 'file',
extension: ['mp4', 'mov'],
success: (res: any) => {
tempFilePath.value = res.tempFiles[0].path;
showVideo.value = true;
form.value.video = '';
}
})
}
onShow(() => {
})
</script>
<style lang="scss" scoped>
.generation-example-item {
position: relative;
--w: 34.6666666667vw;
--h: 22.6666666667vw;
height: var(--h);
width: var(--w);
flex-shrink: 0;
.videocam {
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
.play-icon-image {
width: 30px;
height: 30px;
}
}
}
.footer-placeholder {
--footer-height: 115px;
height: var(--footer-height);
box-sizing: content-box;
.footer {
background: radial-gradient(60% 80% at 50% 100%,
rgba(79, 255, 249, 0.2) 0%,
rgba(181, 226, 255, 0) 80%,
rgba(255, 238, 240, 0) 100%),
radial-gradient(20% 90% at 50% 90%,
rgba(141, 255, 164, 0.3) 0%,
rgba(255, 255, 255, 0) 100%), var(--xl-bg);
height: var(--footer-height);
box-sizing: content-box;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
}
}
</style>
实现页面效果:
文件太大时上传速度很慢。
优化:实现TOS 预签名URL上传
H5和APP端可使用
js
const uploadFile = () => {
uni.showLoading({
title: '上传中...',
mask: true
})
progress.value = 0;
uploadState.value = true;
console.log('是否是微信小程序:', isWeixin.value)
if (isWeixin.value) {
// 微信小程序上传视频
const task = $http.upload('Generation/uploadVideo', {
filePath: tempFilePath.value,
name: 'file',
success: (res: any) => {
uni.hideLoading();
uploadState.value = false;
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
if (data.code === $http.ResponseCode.SUCCESS) {
form.value.video = data.data.url;
nextTick(() => {
showVideo.value = true;
})
} else {
$message.error(data.msg);
}
return;
} catch (e) {
}
}
$message.error('上传失败:' + JSON.stringify(res));
},
fail: (err: any) => {
uni.hideLoading();
uploadState.value = false;
$message.error('上传失败:' + JSON.stringify(err));
},
}, true);
task.onProgressUpdate((res: any) => {
progress.value = res.progress;
console.log('上传进度' + res.progress);
console.log('已经上传的数据长度' + res.totalBytesSent);
console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
});
} else {
// H5和APP视频上传
// 获取预签名URL:uploadTos
console.log('tempFilePath', tempFilePath.value)
let fileName = fileObj.name;
let fileType = fileObj.type;
let fileSize = fileObj.size;
$http.post('Generation/uploadTos', {
data: JSON.stringify({
type: fileType,
name: fileName,
size: fileSize,
})
}).then((res: any) => {
// console.log('获取预签名URL:', res);
if (res.code == 200) {
let url = res.data;
axios({
url: url,
method: "put",
data: fileObj,
contentType: false,
processData: false,
//原生获取上传进度的事件
onUploadProgress: (progressEvent: any) => {
let processTwo = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
progress.value = processTwo * 1;
let progressTex = `上传进度:${processTwo}%`;
console.log(progressTex);
},
}).then((res2: any) => {
if (res2.status !== 200) {
uni.hideLoading();
uploadState.value = false;
$message.error('上传失败');
return
}
// 截取视频地址:
// let responseURL = res2.request.responseURL.split('?')[0]
let responseURL = res2.request.responseURL.split('?')[0]
// 视频上传完成回调后端接口---判断上传成功or失败
$http.post('Generation/uploadAiBack', {
data: { url: responseURL }
}).then((res3: any) => {
// console.log('视频上传完成回调', res3);
// 视频上传成功
if (res3.code === $http.ResponseCode.SUCCESS) {
form.value.video = url;
uni.hideLoading();
uploadState.value = false;
nextTick(() => {
showVideo.value = true;
})
} else {
// 视频上传失败
uni.hideLoading();
uploadState.value = false;
$message.error(res3.msg);
}
})
})
} else {
uni.hideLoading();
uploadState.value = false;
$message.error('获取预签名URL失败');
}
})
}
}
http请求封装:这是开发的项目里的文件,非自己封装的,可用自己的请求方法更改
js
import { useUserStore } from "@/stores";
import { useStorage } from "./storage";
let baseHOST = ''; // 请求地址
let baseURL = `${baseHOST}api/`;
// #ifdef H5
import system from './h5/system';
export const $system = system;
// #endif
// #ifdef MP-WEIXIN
import system from './mp-weixin/system';
export const $system = system;
// #endif
// #ifdef MP-TOUTIAO
import system from './mp-toutiao/system';
export const $system = system;
// #endif
// #ifdef APP
import system from './app/system';
export const $system = system;
// #endif
if (system.isProd()) {
baseHOST = `${system.host()}/app/ycDigitalHuman/`;
baseURL = `${system.host()}/app/ycDigitalHuman/api/`;
}
const RequestHeaders = (options?: any) => {
const header: any = {};
const { getToken, hasLogin } = useUserStore();
if (hasLogin()) {
header['Authorization'] = getToken();
}
if (options && options.header) {
for (let x in options.header) {
header[x] = options.header[x]
}
}
const storeage = useStorage();
const appid = system.appid();
if (appid) {
header['Appid'] = appid;
const icode = storeage.get('ICODE.' + appid);
if (icode) {
header['Icode'] = icode;
}
if (storeage.get('icode')) {
header['Icode'] = storeage.get('icode');
}
const PUID = storeage.get('PUID.' + appid);
if (PUID) {
header['Puid'] = PUID;
}
}
return header;
}
export const $http = {
ResponseCode: {
SUCCESS: 200,
NEED_LOGIN: 12000,
PAY_SUCCESS: 9000,
PAY_NOTPAY: 11000,
NEED_PAY: 60000,
CLOCK_IN: 70000,
},
baseURL,
baseHOST,
RequestHeaders,
image: (path: string) => {
return `${baseHOST}${path}`;
},
get: (url: string, options?: any): Promise<any> => {
return new Promise((resolve, reject) => {
uni.request({
...options,
timeout: 6000000,
header: RequestHeaders(options),
url: baseURL + url,
success: (res: any) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: (err: any) => {
reject(err);
},
complete: () => { }
})
})
},
post: (url: string, options: any): Promise<any> => {
return new Promise((resolve, reject) => {
uni.request({
...options,
timeout: 6000000,
header: RequestHeaders(options),
url: baseURL + url,
method: 'POST',
success: (res: any) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: (err: any) => {
reject(err);
},
complete: () => { }
})
})
},
upload: (url: string, options: any, task?: false): Promise<any> | UniNamespace.UploadTask => {
if (task) {
return uni.uploadFile({
...options,
timeout: 6000000,
header: RequestHeaders(options),
url: baseURL + url
})
} else {
return new Promise((resolve, reject) => {
uni.uploadFile({
...options,
timeout: 6000000,
header: RequestHeaders(options),
url: baseURL + url,
success: (res: any) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (e) {
reject(res);
}
} else {
reject(res);
}
},
fail: (err: any) => {
reject(err);
},
})
})
}
}
}