当前位置: 首页 > news >正文

vue 3 阿里云视频播放器 如何让用户自己给视频添加节点

vue 3 阿里云视频播放器 如何让用户自己给视频添加节点

带节点截图功能的视频标记
1.自定义视频控制条,在进度条上显示节点标记
2.添加截图功能,保存节点时刻的视频画面
3.右侧面板管理所有节点和截图

<template><!-- 成员名单 --><el-dialogv-model="dialogpeopleVisible":before-close="handleClose"class="aliyunplayDialog"@opened="opena"><!-- :show-close = "false" --><!-- <div  class="prism-player" id="playerContainer" ></div> --><div class="container"><div class="dialog-content-self">自定义节点标记</div><!-- <header><h1>Vue 3 阿里云视频播放器 - 自定义节点标记</h1><p>当前时间: {{ currentTimeFormatted }}</p></header> --><div class="main-content"><div class="video-container"><div id="player-container"></div></div><div class="nodes-panel"><div class="panel-header"><div>节点管理</div><span>总数: {{ nodes.length }}</span></div><div class="add-node-form"><div class="form-group"><label>节点标题</label><el-input v-model="newNodeTitle" placeholder="输入节点标题" /></div><div class="form-group"><div style="display: flex;align-items: center;"><label>节点图片</label><el-buttontype="primary"class="btn-screenshot"style="margin-left: 12px;"@click="captureNodeScreenshot($event)">截图</el-button></div><div style="display: flex; flex-direction: column"><el-upload:class="{ isShowImg: newNodeImg.length > 0 }"action="#"list-type="picture-card":auto-upload="false"accept="image/*":limit="1":on-exceed="handleExceed"v-model:file-list="newNodeImg":on-change="handleChange"><el-icon><Plus /></el-icon><template #file="{ file }"><div><imgclass="el-upload-list__item-thumbnail":src="file.url"alt=""/><span class="el-upload-list__item-actions"><spanv-if="!disabled"class="el-upload-list__item-delete"@click="handleRemovePicture(file)"><el-icon><Delete /></el-icon></span></span></div></template></el-upload><div style="font-size: 12px; color: #86909c">{{ $t("view.course.the_image_size_cannot_exceed_2mb!") }}</div></div></div><div class="form-group"><label>节点内容</label><el-input:rows="3"type="textarea"v-model="newNodeContent"placeholder="输入节点内容(可选)"></el-input></div><div style="display: flex;align-items: center;"><el-button type="primary" @click="addNode">添加节点</el-button><el-button type="primary" @click="completeNode">完成</el-button></div></div><div class="nodes-list" v-if="sortedNodes.length > 0"><div v-if="sortedNodes.length === 0" class="empty-state"><p>暂无节点,点击上方按钮添加</p></div><divv-for="node in sortedNodes":key="node.id"class="node-item"@click="jumpToNode(node.time)"><div class="node-header"><span class="node-time">{{ node.timeFormatted }}</span><button class="btn-delete" @click="deleteNode(node.id, $event)">删除</button></div><div>{{ node.title }}</div><div class="node-content">{{ node.content }}</div></div></div></div></div><!-- <div class="instructions"><h3>使用说明</h3><ul><li>播放视频到想要添加节点的位置,暂停或继续播放</li><li>填写节点标题和内容(可选)</li><li>点击"添加节点"按钮,节点将保存在当前时间点</li><li>在右侧面板点击任意节点可以跳转到对应时间点</li><li>可以删除不需要的节点</li></ul></div> --></div></el-dialog>
</template><script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue";
import CustomProgressComponent from "@/components/course/CustomProgressComponent/index";
import { ElMessage, ElMessageBox } from "element-plus";const dialogpeopleVisible = ref(false);const infoValue = ref({});const player = ref(null);
const nodes = ref([]);
const newNodeTitle = ref("");
const newNodeContent = ref("");
const newNodeImg = ref([]);
const currentTime = ref(0);
const videoElement = ref(null);const emit = defineEmits(["close","completeNode"]);
const openDialog = (bool, info) => {console.log(info, "info");dialogpeopleVisible.value = bool;infoValue.value = info;
};const opena = () => {initPlayer();
};// 更新播放器的进度标记
const updatePlayerMarkers = () => {if (player.value) {player.value.setProgressMarkers(nodes.value);}
};
// 初始化播放器
const initPlayer = () => {if (player.value) {// 如果已经创建了,就销毁player.value.dispose();player.value = null;}player.value = new Aliplayer({id: "player-container",source: infoValue.value.url.replace(/^http:\/\//i, "https://"), // 替换为实际视频URLwidth: "100%",height: "500px",autoplay: false,// isLive: false,// rePlay: false,// playsinline: true,// preload: true,// controlBarVisibility: "hover",// useH5Prism: true,skinLayoutIgnore: ["loading"],//对播放按钮位置修改skinLayout: [{ name: "bigPlayButton", align: "cc", x: 30, y: 80 },{name: "H5Loading",align: "cc",x: 30,y: 80,},{name: "controlBar",align: "blabs",x: 0,y: 0,children: [{name: "progress",align: "tlabs",x: 0,y: 0,},{ name: "playButton", align: "tl", x: 15, y: 12 },{ name: "timeDisplay", align: "tl", x: 10, y: 6 },{ name: "fullScreenButton", align: "tr", x: 10, y: 12 },{ name: "volume", align: "tr", x: 10, y: 10 },// { name: "setting", align: "tr", x: 10, y: 12 }],},{name: "fullControlBar",align: "tlabs",x: 0,y: 0,children: [{ name: "fullTitle", align: "tl", x: 25, y: 6 },{ name: "fullNormalScreenButton", align: "tr", x: 24, y: 13 },{ name: "fullTimeDisplay", align: "tr", x: 10, y: 12 },{ name: "fullZoom", align: "cc" },],},],progressMarkers: nodes.value,components: [{name: "CustomProgressComponent",type: CustomProgressComponent,},],},() => {console.log("播放器初始化完成");// 获取视频DOM元素setTimeout(() => {const playerWrap = document.getElementById("player-container");videoElement.value = playerWrap.querySelector("video");}, 1000);});player.value.on("timeupdate", () => {console.log(player.value.getCurrentTime(), "player.value.getCurrentTime()");currentTime.value = player.value.getCurrentTime();});
};// 添加节点
const addNode = () => {if (!newNodeTitle.value.trim()) {ElMessage.error("请输入节点标题");return;}const time = currentTime.value;const timeFormatted = formatTime(time);nodes.value.push({id: Date.now(),time: time,timeFormatted: timeFormatted,title: newNodeTitle.value,content: newNodeContent.value,offset: time,describe: newNodeContent.value,coverUrl:newNodeImg.value.length > 0? URL.createObjectURL(newNodeImg.value[0].raw): "",isCustomized: true,});console.log(nodes.value, "nodes.value");// 更新播放器的进度标记updatePlayerMarkers();// 清空表单newNodeTitle.value = "";newNodeContent.value = "";newNodeImg.value = [];
};//视频节点完成处理
const completeNode = () =>{emit('completeNode',infoValue.value )handleClose()
}
// 删除节点
const deleteNode = (id, event) => {event.stopPropagation();nodes.value = nodes.value.filter((node) => node.id !== id);// 更新播放器的进度标记updatePlayerMarkers();
};// 跳转到节点时间点
const jumpToNode = (time) => {if (player.value) {player.value.seek(time);}
};// 格式化时间
const formatTime = (seconds) => {const mins = Math.floor(seconds / 60);const secs = Math.floor(seconds % 60);return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
};// 当前时间格式化显示
const currentTimeFormatted = computed(() => {return formatTime(currentTime.value);
});// 按时间排序节点
const sortedNodes = computed(() => {return [...nodes.value].sort((a, b) => a.time - b.time);
});const handleExceed = (files) => {ElMessage({type: "error",message: t("view.course.only_one_cover_image_can_be_uploaded"),});
};
const handleChange = (data, fileList) => {let file = data.raw;const isJpgPng =file.type === "image/jpeg" ||file.type === "image/png" ||file.type === "image/gif";if (!isJpgPng) {ElMessage({message: t("view.course.the_uploaded_file_format_can_only_be_jpg/png/gif"),type: "warning",});const currIdx = fileList.indexOf(file);fileList.splice(currIdx, 1);return false;}const isLt2M = file.size / 1024 / 1024 < 10;if (!isLt2M) {ElMessage({message: t("view.course.the_avatar_image_size_cannot_exceed_2mb!"),type: "warning",});const currIdx = fileList.indexOf(file);fileList.splice(currIdx, 1);return false;}return isJpgPng && isLt2M;
};const handleRemovePicture  = (file) => {console.log(file,'file');newNodeImg.value = newNodeImg.value.filter((item) => item.name !== file.name); 
}
// base64转file
const base64ToFile = (base64, filename = "") => {const arr = base64.split(",");let mime = arr[0].match(/:(.*?);/)[1]; // 匹配出图片类型mime = mime.replace("data:", ""); // 去掉data:image/png;base64 // 去掉url中的base64,并转化为Uint8Array类型const bstr = atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new File([u8arr], filename, { type: mime });
};
// 为已有节点捕获截图
const captureNodeScreenshot = (event) => {// event.stopPropagation();if (!videoElement.value) {alert("视频元素未准备好,请稍后再试");return;}// 等待视频跳转完成setTimeout(() => {// 创建canvas来捕获视频帧const canvas = document.createElement("canvas");canvas.width = videoElement.value.videoWidth;canvas.height = videoElement.value.videoHeight;const ctx = canvas.getContext("2d");ctx.drawImage(videoElement.value, 0, 0, canvas.width, canvas.height);// 将canvas转换为数据URLconst screenshotData = canvas.toDataURL("image/png");let  rawFileAvatar = base64ToFile(screenshotData, "avatar.png")// console.log(screenshotData,'screenshotData');let imageUrl = URL.createObjectURL(rawFileAvatar);newNodeImg.value.push({name: '节点图片',url: imageUrl,raw: rawFileAvatar,});}, 300);
};const handleClose = () => {dialogpeopleVisible.value = false;// 清空节点数据,这样 sortedNodes 计算属性会自动更新为空数组nodes.value = [];player.value?.dispose(); // 修正变量名player.value = null;emit("closeNodes");
};
onUnmounted(() => {// 清空节点数据,这样 sortedNodes 计算属性会自动更新为空数组nodes.value = [];player.value?.dispose();player.value = null;emit("closeNodes");
});
defineExpose({ openDialog, handleClose });
</script>
<style lang="scss">
.el-dialog.aliyunplayDialog {.el-dialog__header {padding-top: 0px !important;}.el-dialog__body {padding-top: 10px !important;}
}.dialog-content-self {font-size: 20px;// font-weight: 600;color: rgb(29, 33, 41);// margin-bottom: 20px;
}.prism-player {width: 750px;height: 522px;
}
</style>
<style>
.container {max-width: 1200px;margin: 0 auto;display: flex;flex-direction: column;gap: 20px;/* max-height: 500px; */
}header {text-align: center;padding: 15px 0;background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);color: white;border-radius: 10px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}.main-content {display: flex;gap: 20px;
}.video-container {flex: 3;background: white;border-radius: 10px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);overflow: hidden;height: 500px;
}#player-container {width: 100%;height: 400px;
}.nodes-panel {flex: 1;background: white;border-radius: 10px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);padding: 20px;display: flex;flex-direction: column;overflow-y: auto;height: 500px;
}.panel-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 15px;padding-bottom: 10px;border-bottom: 1px solid #eee;
}.add-node-form {display: flex;flex-direction: column;gap: 10px;margin-bottom: 20px;padding: 15px;background: #f9f9f9;border-radius: 8px;
}.form-group {display: flex;flex-direction: column;gap: 5px;
}label {font-weight: 500;font-size: 14px;
}input,
textarea {padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;
}textarea {min-height: 60px;resize: vertical;
}
.btn-screenshot{padding: 10px;height: 0px;
}
button {padding: 8px 16px;background: #4a6cf7;color: white;border: none;border-radius: 4px;cursor: pointer;font-weight: 500;transition: background 0.3s;
}.btn-delete {background: #f44336;
}.btn-delete:hover {background: #e53935;
}.nodes-list {flex: 1;/* overflow-y: auto; */
}.node-item {padding: 12px;border-radius: 6px;background: #f9f9f9;margin-bottom: 10px;cursor: pointer;transition: background 0.2s;
}.node-item:hover {background: #f0f4ff;
}.node-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 5px;
}.node-time {font-weight: 600;color: #4a6cf7;
}.node-content {font-size: 14px;color: #555;
}.empty-state {text-align: center;padding: 30px;color: #888;
}.instructions {background: white;border-radius: 10px;padding: 20px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}.instructions h3 {margin-bottom: 10px;color: #4a6cf7;
}.instructions ul {padding-left: 20px;
}.instructions li {margin-bottom: 8px;font-size: 14px;
}@media (max-width: 768px) {.main-content {flex-direction: column;}#player-container {height: 300px;}
}.isShowImg {.el-upload-list__item.is-ready {width: 160px;height: 90px;border: none;}.el-upload-list__item.is-success {width: 160px;height: 90px;border: none;}.el-upload--picture-card {display: none;}
}
</style>

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


文章转载自:

http://oFmUrS9M.yswxq.cn
http://24vVkG4O.yswxq.cn
http://ZI7m1UXU.yswxq.cn
http://22VQsiNW.yswxq.cn
http://yPhyZPbx.yswxq.cn
http://oi15l4In.yswxq.cn
http://UZ7IuTf5.yswxq.cn
http://rOYHD8p4.yswxq.cn
http://gNMLXozV.yswxq.cn
http://7sggvjiV.yswxq.cn
http://30wMQNtD.yswxq.cn
http://peFrqho7.yswxq.cn
http://aruxDBJu.yswxq.cn
http://xtK3XyzW.yswxq.cn
http://GdrEQcOo.yswxq.cn
http://Qsg3yCNl.yswxq.cn
http://EslCsecU.yswxq.cn
http://WSJC2fkD.yswxq.cn
http://VHIrUZgw.yswxq.cn
http://bV1Lj6ri.yswxq.cn
http://W6q8eP57.yswxq.cn
http://j7bgH8BZ.yswxq.cn
http://t25hehuL.yswxq.cn
http://7sCUJ6YX.yswxq.cn
http://3H6OXCkC.yswxq.cn
http://Iwz40Jfw.yswxq.cn
http://khTCELa4.yswxq.cn
http://BIw2RZBt.yswxq.cn
http://rxnBgMV6.yswxq.cn
http://tgM0DoAK.yswxq.cn
http://www.dtcms.com/a/386430.html

相关文章:

  • LinkedList 底层实现与 ArrayList 对比分析
  • 滚珠花键在半导体制造设备中承担怎样的核心功能?
  • 服装制造企业痛点解决方案:EFISH-SBC-RK3588 柔性化吊挂调度方案
  • 10cm钢板矫平机:工业制造中的“整形医生”
  • html表单登录模式代码
  • QUIC 协议域名封堵:核心原理、关键技术与实现路径(C/C++代码实现)
  • 8 基于机器学习进行遥感影像的地物分类-以随机森林为例
  • Qt读写SQLite示例
  • Jmeter性能测试之阶梯式场景、波浪式场景、服务器监控
  • 黄昏时刻复古胶片风格人像风光摄影后期Lr调色教程,手机滤镜PS+Lightroom预设下载!
  • Django ORM多对多关系实战指南
  • 【从零开始java学习|第十七篇】面向对象进阶
  • Three.js 开发实战教程(一):环境搭建与第一个 3D 场景
  • 旅游小程序的功能优势
  • LeetCode:7.接雨水
  • Android 安卓 问题解决记录 腾讯IM和厂商离线推送问题 点击离线推送无法唤醒APP启动页但某些Service服务和Application被启动
  • 动态规划解决系列子序列问题
  • SCADE One vs Scade 6 - 标量积建模比较
  • Next.js 身份验证与授权:使用 NextAuth.js 保护你的应用
  • Spring MVC 的案例小练习
  • 贪心算法与动态规划
  • 香港期权市场的主要参与者有哪些?
  • 系统中间件与云虚拟化-serverless-基于阿里云函数计算的简单邮件发送服务设计与体验
  • 【LLM】GPT-OSS架构变化详解
  • 【开题答辩全过程】以 “寄情绿苑”绿色殡葬服务小程序的设计和实现为例,包含答辩的问题和答案
  • 容器化部署之dockerfile07
  • 一篇读懂Pormise!!【前端ES6】
  • spring-kafka的消息过滤器RecordFilterStrategy
  • gin中sse流式服务
  • 论文笔记(九十一)GWM: Towards Scalable Gaussian World Models for Robotic Manipulation