express+vue在线im实现【三】

news2025/3/13 13:22:14

往期内容

express+vue在线im实现【一】
express+vue在线im实现【二】

本期示例

在这里插入图片描述

在这里插入图片描述

本期总结

  • 支持各种类型的文件上传,常见文件类型图片,音频,视频等,上传时同步获取音频与视频的时长,以及使用上传文件的缓存路径来作为video播放地址,使用canvas生成视频的第一帧作为封面(本期的第一个亮点)
  • 使用腾讯播放器完成视频播放,支持自定义控件功能与样式,这儿示例了前进后退15s与设置里的配置(本期的第二个亮点)
  • 音频使用html5标签audio播放
  • 其他类型统一为下载

下期安排

  • 在线音频录制,发送
  • 在线语音

重点总结

上传部分

input chang方法

        // 有上传文件
        inputFileChange(e) {
            let file = e.target.files[0]

            if (!file) return
            let { type } = file
            // 图片
            if (type.indexOf('image') >= 0) {
                this.commonUploadImg(file, 'im')
                    .then(({ url }) => {
                        // 发送消息
                        this.pushInfo({
                            msg_type: '2',
                            content: url,
                        })
                    })
                    .catch(() => {})
                    .finally(() => {
                        this.$refs.fileInput.value = ''
                    })

                return
            }

            // mp4
            if (type.indexOf('video') >= 0) {
                if (type != 'video/mp4') {
                    this.$message.warning('请上传mp4格式的视频')
                    return
                }
                // 视频上传
                this.commonUploadFile(file, 'im', 500)
                    .then(({ url = '' }) => {
                        // 发送消息
                        this.pushInfo({
                            msg_type: '4',
                            content: url,
                        })
                    })
                    .catch(() => {})

                this.$refs.fileInput.value = ''
                return
            }

            // audio
            if (type.indexOf('audio') >= 0) {
                if (type != 'audio/ogg') {
                    this.$message.warning('请上传audio/ogg格式的音频')
                    return
                }
                this.commonUploadFile(file, 'im', 500)
                    .then(({ url = '' }) => {
                        // 发送消息
                        this.pushInfo({
                            msg_type: '5',
                            content: url,
                        })
                    })
                    .catch(() => {})

                this.$refs.fileInput.value = ''
                return
            }

            // 其他类型
            this.commonUploadFile(file, 'im', 500)
                .then(({ url = '' }) => {
                    // 发送消息
                    this.pushInfo({
                        msg_type: '3',
                        content: url,
                    })
                })
                .catch(() => {})
            this.$refs.fileInput.value = ''
        },

统一的上传方法

/**
 * 公共上传图片方法(相比下面的上传文件方法,多了压缩与获取图片宽高)
 * @param {*} oldFile  文件信息
 * @param {*} type     服务器的存储位置
 * @param {*} minSize  最小产生loading的文件大小
 * @returns
 */
export function commonUploadImg(oldFile, type, minSize = 500) {
    return new Promise(async (resolve, reject) => {
        let { size } = oldFile
        // 对于大于200k的图片添加一个loading
        const currentSize = size / 1024
        let loading = null
        if (currentSize > minSize) {
            loading = this.$klLoading()
        }
        try {
            let { file: miniFile, newWidth, newHeight } = await compressImg(oldFile)
            const formData = new FormData()
            formData.append('file', miniFile)
            const devicePixelRatioa = window.devicePixelRatio || 1

            // 上传图片,同时需要上传图片的宽高
            upload_imgs_im(formData, {
                type,
                devicePixelRatioa,
                width: Math.floor(newWidth / devicePixelRatioa),
                height: Math.floor(newHeight / devicePixelRatioa),
            }).then((res) => {
                resolve({ url: `/${type}/` + res.data[0]?.filename })
            })
        } catch (err) {
            this.$message.warning('请重新上传')
            reject()
        }
        loading && loading.close()
    })
}

/**
 * 公共上传通用文件的方法
 * @param {*} oldFile  文件信息
 * @param {*} type     服务器的存储位置
 * @param {*} minSize  最小产生loading的文件大小
 * @param {*} needPoster   对于视频需要上传封面图,这个用于获取封面图
 * @returns
 * */
export function commonUploadFile(oldFile, type, minSize = 500, needPoster = false) {
    return new Promise(async (resolve, reject) => {
        let { size = 0 } = oldFile
        // 对于大于minSize的图片添加一个loading
        let loading = null
        if (size / 1024 > minSize) {
            loading = this.$klLoading()
        }
        const formData = new FormData()
        formData.append('file', oldFile)

        // 封面图对象
        let preImg = {}
        if (needPoster) {
            try {
                // 获取文件的缓存地址
                const file_path = getObjectURL(oldFile)
                // 获取视频首帧的图片宽高及babs64图片
                const { width, height, pre_img, duration } = await getVideoCover(file_path)
                // 封面图toFile
                let file = this.base64ToFile(pre_img, createId())
                // 上传封面图
                let res = await commonUploadImg(file, 'im', 5000).catch(() => {
                    return {}
                })

         
                const poster = res.url || ''
                Object.assign(preImg, {
                    video_width: width,
                    video_height: height,
                    poster,
                    time: duration,
                })
            } catch (err) {
                console.log('err', err)
                this.$message.error('获取封面失败,请重试~')
                reject(err)
                loading && loading.close()
                return
            }
        }

        let res = await upload_imgs_im(formData, {
            type,
        }).catch((err) => {
            console.log('err', err)
            return {}
        })
        resolve({ url: `/${type}/` + res.data[0]?.filename, ...preImg })
        loading && loading.close()
    })
}

// 富文本给图片补充完整路径
export function parseHtmlUrl(htmlString) {
    function removeTrailingSlash(str) {
        if (str.endsWith('/')) {
            return str.slice(0, -1) // 使用slice方法从字符串的开头到倒数第二个字符(不包括结尾的/)
        }
        return str // 如果字符串不以/结尾,则直接返回原字符串
    }
    const { origin } = location
    const baseurl = removeTrailingSlash(baseURL)
    const parser = new DOMParser()
    const doc = parser.parseFromString(htmlString, 'text/html')
    const imgs = doc.body.querySelectorAll('img')
    for (let index = 0; index < imgs.length; index++) {
        const element = imgs[index]
        const { src } = element
        if (src && src.startsWith('/') && !src.startsWith('//')) {
            element.src = baseurl + element.src
        }
    }

    const serializer = new XMLSerializer()
    const modifiedHtml = serializer.serializeToString(doc.body)
    // 移除外层的body
    let div = document.createElement('div')
    div.innerHTML = modifiedHtml
    return div.innerHTML
}

// 截取视频的封面图
export function getVideoCover(url) {
    if (!url) return
    return new Promise((resolve, reject) => {
        let dataURL = ''
        let video = document.createElement('video')
        video.setAttribute('crossOrigin', 'anonymous') //处理跨域
        video.setAttribute('src', url)
        video.setAttribute('autoplay', 'true')
        video.setAttribute('muted', 'true')
        video.setAttribute('playsinline', 'true')
        video.setAttribute('webkit-playsinline', 'true')
        video.setAttribute('x5-video-player-type', 'h5')
        // 设置时间为第一秒
        video.currentTime = 1
        // 播放错误监听
        video.addEventListener('error', (err) => {
            video.remove()
            reject(err)
        })

        // 兼容ios的上传,改成了延时获取
        let timer = setTimeout(() => {
            // 获取宽高
            let { videoWidth, videoHeight } = video
            // 创建canvas 取视频的第一帧作为封面图
            let canvas = document.createElement('canvas')
            canvas.width = videoWidth
            canvas.height = videoHeight
            let ctx = canvas.getContext('2d')
            ctx.drawImage(video, 0, 0, videoWidth, videoHeight)
            dataURL = canvas.toDataURL('image/jpeg')
            // 获取成功后清除节点
            video.remove()
            timer = null
            clearTimeout(timer)
            resolve({
                width: videoWidth || 0,
                height: videoHeight || 0,
                pre_img: dataURL,
                duration: video.duration || 0,
            })
        }, 1000)
    })
}

// 获取视频的本地地址
export function getObjectURL(file) {
    var url = null
    // 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
    if (window.createObjectURL !== undefined) {
        // basic
        url = window.createObjectURL(file)
    } else if (window.URL !== undefined) {
        // mozilla(firefox)
        url = window.URL.createObjectURL(file)
    } else if (window.webkitURL !== undefined) {
        // webkit or chrome
        url = window.webkitURL.createObjectURL(file)
    }
    return url
}

视频播放组件

核心播放组件

<template>
    <div class="demo">
        <video
            id="player-container-id"
            preload="auto"
            playsinline
            webkit-playsinline
            class="tx-video"
            :style="getStyle"
        ></video>
    </div>
</template>

<script>
import { createControl } from './index.js'
const plugins = [
    {
        isAppendHead: true,
        css: 'https://web.sdk.qcloud.com/player/tcplayer/release/v5.0.1/tcplayer.min.css',
    },
    {
        js: 'https://web.sdk.qcloud.com/player/tcplayer/release/v5.0.1/tcplayer.v5.0.1.min.js',
    },
]
export default {
    props: {
        videoUrl: {
            type: String,
            default: '',
        },
        width:{
            type: String,
            default: '600px',
        },
        height:{
            type: String,
            default:'400px',
        }
    },
    data() {
        return {
            list: [1111],
        }
    },
    computed: {
        player() {
            let { videoStore } = this.$store.state
            return videoStore.player || {}
        },
        getStyle(){
            return {
                width:this.width,
                height:this.height,
            }
        }
    },
    mounted() {
        this.getIndexDBJS(plugins).finally(() => {
            this.init()
        })
    },
    beforeDestroy() {
        this.player.dispose()
        this.delPageScript(plugins)
    },
    methods: {
        async init() {
            this.$store.commit('videoStore/SET_PLAYER', null)
            let { player } = this
            if (player && this.getType(player.dispose) === 'function') {
                // 先销毁
                this.player.dispose()
                await this.$nextTick()
            }
            player = TCPlayer('player-container-id', {
                sources: [
                    {
                        src: this.videoUrl,
                    },
                ],
                licenseUrl: this.videoUrl,
            })
            player.src(this.videoUrl)
            player.on('loadedmetadata', () => {
                // 视频加载完成-设置控件
                createControl(this)
            })

            this.$store.commit('videoStore/SET_PLAYER', player)
        },
    },
}
</script>

<style scoped>
/deep/ .tcp-skin .vjs-custom-control-spacer {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
</style>

控件index.js入口

// 前进后退控件
import fast_forward from './components/fast_forward.vue'

// 系统控制控件
import sys from './components/sys.vue'

export function createControl(that) {
    // 处理前进后退的播放控件
    const Ctor = Vue.extend(fast_forward)
    // create 可以传入props值
    const comp = new Ctor({
        propsData: {
            preImg: '//image.zlketang.com/public/news/others/imgs/web_pc/0283cad753b8be5df7a764d78f66dd31.png',
            nextImg:
                '//image.zlketang.com/public/news/others/imgs/web_pc/5510ac8bad62f39b6675a12574347598.png',
        },
    })
    comp.$mount()
    let controlBox = document.querySelector('.vjs-custom-control-spacer')
    if (!controlBox) return
    // 清空controlBox下的数据
    controlBox.innerHTML = ''

    controlBox.appendChild(comp.$el)
    // 监听组件的emit事件
    comp.$on('pre-fun', (data) => {
        console.log('pre-fun', data, that.list)
    })
    comp.$on('next-fun', (data) => {
        console.log('next-fun', data, that.list)
    })
    const sysCtor = Vue.extend(sys)
    // create 可以传入props值
    const sysComp = new sysCtor({
        propsData: {},
    })

    sysComp.$mount()
    controlBox.appendChild(sysComp.$el)
}

具体实现fast_forward.vue示例

<template>
    <!-- 前进后退15s控件  -->
    <div class="tx-video-control-fast-forward flex-center-wrap">
          <img
            @click="pre"
            class="backward-box-img"
            :src="preImg"
        />
        <img
            @click="next"
            class="forward-box-img"
            :src="nextImg"
        />
    </div>
</template>

<script>
export default {
    name: 'tx-video-control-fast-forward',
    props:{
        preImg:{
            type:String,
            default:''
        },
        nextImg:{
            type:String,
            default:''
        }
    },
    data() {
        return {}
    },
    methods: {
        pre() {
            this.$emit('pre-fun')
        },
        next() {
            this.$emit('next-fun')
        },
    },
}
</script>

<style scoped>
.backward-box-img {
    cursor: pointer;
    width: 20px;
    height: 20px;
}
.forward-box-img {
    cursor: pointer;
    width: 20px;
    height: 20px;
    margin-left: 24px;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1850872.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

天马学航——智慧教务系统(移动端)开发日志六

天马学航——智慧教务系统(移动端)开发日志六 日志摘要&#xff1a;统一身份认证设计&#xff0c;修复了选课信息错乱的问题 界面设计 实现思路 使用 Java 和 Jedis 完成实现&#xff1a; 步骤一&#xff1a;添加 Jedis 依赖 首先需要在项目中添加 Jedis 依赖&#xff0c;…

已解决VirtualMachineError: 虚拟机错误的正确解决方法,亲测有效!!!

已解决VirtualMachineError: 虚拟机错误的正确解决方法&#xff0c;亲测有效&#xff01;&#xff01;&#xff01; 目录 问题分析 报错原因 解决思路 解决方法 分析错误日志 优化代码 内存泄漏排查 优化递归调用 调整JVM参数 使用监控工具 增加物理内存或升级硬件…

芝麻清单助力提升学习工作效率 专注时间完成有效的待办事项

芝麻清单助力提升学习&工作效率 专注时间完成有效的工作。今天我们给大家带来一个专注清单&#xff0c;一个更高效的学习和工作的方法&#xff01; 我们都知道&#xff0c;专注做一个事情&#xff0c;会有效的提升效率&#xff0c;让事情更高效的完成。如果是学习的话&…

java基于ssm+jsp 母婴用品网站

1管理员功能模块 管理员登录&#xff0c;管理员通过输入用户名、密码等信息进行系统登录&#xff0c;如图1所示。 图1管理员登录界面图 管理员登录进入母婴用品网站可以查看主页、个人中心、用户管理、商品分类管理、商品信息管理、留言板管理、成长交流、系统管理、订单管理、…

Springboot应用的信创适配-补充

Springboot应用的信创适配-CSDN博客 因为篇幅限制&#xff0c;这里补全Spring信创适配、数据库信创适配、Redis信创适配、消息队列信创适配等四个章节。 Springboot应用的信创适配 Springboot应用的信创适配&#xff0c;如上图所示需要适配的很多&#xff0c;从硬件、操作系统、…

vue3 computed与watch,watchEffect比较

相同点 都是要根据一个或多个响应式数据进行监听 不同点 computed 如要return回来一个新的响应式值&#xff0c;且这个值不允许直接修改&#xff0c;想要修改的话可以设置set函数&#xff0c;在函数里面去修改所依赖的响应式数据&#xff0c;然后计算属性值会基于其响应式依…

多功能投票系统(ThinkPHP+FastAdmin+Uniapp)

让决策更高效&#xff0c;更民主&#x1f31f; ​基于ThinkPHPFastAdminUniapp开发的多功能系统&#xff0c;支持图文投票、自定义选手报名内容、自定义主题色、礼物功能(高级授权)、弹幕功能(高级授权)、会员发布、支持数据库私有化部署&#xff0c;Uniapp提供全部无加密源码…

ffmpeg音视频开发从入门到精通——ffmpeg实现音频抽取

文章目录 FFmpeg 实现音频流抽取1. 包含FFmpeg头文件与命名空间声明2. 主函数与参数处理3. 打开输入文件4. 获取文件信息5. 查找音频流6. 分配输出文件上下文7. 猜测输出文件格式8. 创建新的音频流9. 打开输出文件10. 写入文件头信息11. 读取并写入音频数据12. 写入文件尾部信息…

vue中的状态管理

第1部分&#xff1a;引言 状态管理是应用中数据流动和变更的核心机制。在Vue应用中&#xff0c;状态管理不仅涉及到组件间的数据共享&#xff0c;还包括了数据的持久化、异步操作的处理等复杂场景。良好的状态管理策略可以提高应用的响应速度&#xff0c;降低组件间的耦合度&a…

经典游戏案例:植物大战僵尸

学习目标&#xff1a;植物大战僵尸核心玩法实现 游戏画面 项目结构目录 部分核心代码 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using Random UnityEngine.Random;public enum Z…

(2024)豆瓣电影详情页内容爬虫详解和源码

&#xff08;2024&#xff09;豆瓣电影详情页内容爬虫详解和源码 这是一个Python爬虫程序&#xff0c;用于抓取豆瓣电影详情页面如https://movie.douban.com/subject/1291560/的数据。它首先发送GET请求&#xff0c;使用PyQuery解析DOM&#xff0c;然后根据<br>标签分割H…

C语言第17篇:预处理详解

1、预定义符号 C语言设置了一些预定义符号&#xff0c;可以直接使用。预定义符号也是在预处理期间处理的。 __FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI…

LaTeX中添加矩阵分块虚线并设置虚线疏密

对于大型矩阵&#xff0c;有时需要添加分块虚线。 方法为使用arydshln宏包&#xff0c;然后在array环境中设置虚线。需要注意的是&#xff0c;使用矩阵环境需要搭配amsmath宏包使用&#xff0c;且需放在amsmath宏包之后。即导言区设置为 \usepackage{amsmath} \usepackage{ary…

人人讲视频如何下载

一、工具准备 1.VLC media player 2.谷歌浏览器 二、视频下载 1.打开人人讲网页&#xff0c;需要下载的视频 谷歌浏览器打开调试窗口 搜索m3u8链接 拷贝到VLCplayer打开网络串流方式打开测试是否能正常播放 2.下载视频 能正常播放后&#xff0c;切换播放为转换选择mp4格式…

【CPP】归并排序

目录 1.归并排序简介代码分析归并的非递归形式 1.归并排序 归并排序&#xff08;MERGE-SORT&#xff09; 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。 将已有序的子序列合并&#xff0c;得到…

扩散模型 GLIDE:35 亿参数的情况下优于 120 亿参数的 DALL-E 模型

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

com域名注册多少钱

COM域名注册价格视具体注册商而定&#xff0c;不同的注册商可能会有不同的收费标准。一般来说&#xff0c;COM域名注册价格在10美元到20美元之间&#xff0c;可根据不同的需求选择注册时间的长短&#xff0c;从1年到10年等不同时间段的注册费用也不同。以下是关于COM域名注册价…

Python爬虫学习 | Scrapy框架详解

一.Scrapy框架简介 何为框架&#xff0c;就相当于一个封装了很多功能的结构体&#xff0c;它帮我们把主要的结构给搭建好了&#xff0c;我们只需往骨架里添加内容就行。scrapy框架是一个为了爬取网站数据&#xff0c;提取数据的框架&#xff0c;我们熟知爬虫总共有四大部分&am…

【LLM之KG】KoPA论文阅读笔记

研究背景 知识图谱补全&#xff08;KGC&#xff09;是通过预测知识图谱中缺失的三元组来完善知识图谱的信息。传统方法主要基于嵌入和预训练语言模型&#xff0c;但这些方法往往忽视了知识图谱的结构信息&#xff0c;导致预测效果不佳。 研究目标 本文的研究目标是探索如何将…

海外短剧系统如何征服观众心

海外短剧系统要征服观众的心&#xff0c;需要综合考虑多个方面。 1、紧凑的剧情设计&#xff1a; 短小精悍&#xff1a;海外短剧通常每集时长不超过半小时&#xff0c;甚至有的仅有几分钟。这种紧凑的剧情设计让观众能够在短时间内迅速沉浸在故事中&#xff0c;无需花费大量时间…