express+vue在线im实现【三】

往期内容

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>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

51-60 CVPR 2024 最佳论文 | Generative Image Dynamics

在2023年11月&#xff0c;谷歌研究院发布了一项令人瞩目的研究成果——Generative Image Dynamics&#xff08;生成图像动力学&#xff09;。这项技术的核心是将静态的图片转化为动态的、无缝循环的视频&#xff0c;而且更令人兴奋的是&#xff0c;这些生成的视频还具有交互性。…

蓝牙ble数传芯片推荐,TD5327A芯片蓝牙5.1—拓达半导体

蓝牙数传芯片TD5327A芯片是一款支持蓝牙BLE的纯数传芯片&#xff0c;蓝牙5.1版本。芯片的亮点在于性能强&#xff0c;除了支持APP端直接对芯片做设置与查询操作&#xff0c;包括直接操作蓝牙芯片自身的IO与PWM口以外&#xff0c;还支持RTC日历功能&#xff0c;可以做各类定时类…

LeetCode:经典题之141、142 题解及延伸

系列目录 88.合并两个有序数组 52.螺旋数组 567.字符串的排列 643.子数组最大平均数 150.逆波兰表达式 61.旋转链表 160.相交链表 83.删除排序链表中的重复元素 389.找不同 1491.去掉最低工资和最高工资后的工资平均值 896.单调序列 206.反转链表 92.反转链表II 141.环形链表 …

Ps:转换为配置文件

Ps菜单&#xff1a;编辑/转换为配置文件 Edit/Convert to Profile 转换为配置文件 Convert to Profile命令可用于在不同色彩空间之间转换图像的颜色配置文件&#xff0c;从而确保在不同设备和介质上颜色的一致性和准确性。 ◆ ◆ ◆ 工作原理说明 当将图像的配置文件从一种转…

秒懂双亲委派机制

前言 最近知识星球中&#xff0c;有位小伙伴问了我一个问题&#xff1a;JDBC为什么会破坏双亲委派机制&#xff1f; 这个问题挺有代表性的。 双亲委派机制是Java中非常重要的类加载机制&#xff0c;它保证了类加载的完整性和安全性&#xff0c;避免了类的重复加载。 这篇文…

北斗三号短报文通信终端 | 助力户外无网络场景作业

北斗三号短报文通信终端是一款专为户外无网络场景作业设计的先进通信工具&#xff0c;它依托于中国自主研发的北斗卫星导航系统&#xff0c;为用户在偏远地区或无网络覆盖区域提供了可靠的通信保障。以下是关于北斗三号短报文通信终端的详细介绍&#xff1a; 一、功能特点 北斗…

[Python人工智能] 四十六.PyTorch入门 (1)环境搭建、神经网络普及和Torch基础知识

从本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。前文讲解合如何利用keras和tensorflow构建基于注意力机制的CNN-BiLSTM-ATT-CRF模型,并实现中文实体识别研究。这篇文章将介绍PyTorch入门知识。前面我们的Python人工智能主要以TensorFlow和Keras为主,…

JavaWeb系列十六: jQuery初步入门

跟老韩-JavaScript开发利器之jQuery 1.1 原理示意图1.2 快速入门1.2 什么是jquery对象1.3 dom对象转jQuery对象1.4 jQuery对象转dom对象 jQuery是一个快速的, 简洁的javaScript库, 使用户能更方便地处理HTML, css, dom…提供方法, events, 选择器, 并且方便地为网站提供AJAX交互…

FFmpeg交叉编译报错pkg-config not found

ffmpeg交叉编译时报错&#xff1a; WARNING: arm-linux-gnueabihf-pkg-config not found, library detection may fail.不慌&#xff0c;没有就下载嘛&#xff0c;直接install&#xff1a; sudo apt-get install pkg-config-arm-linux-gnueabihf 参考&#xff1a; How To I…

无水蒸汽室的热特性​研究

更多资讯&#xff0c;请关注公众号【莱歌数字】~~ 扩散电阻在从源到汇的整体传热过程中继续起着主导作用。 随着电子元件占地面积小和高功耗的趋势&#xff0c;需要在散热器的底部散热对于降低扩散电阻变得非常重要。 在一些应用中&#xff0c;如高功率激光器&#xff0c;可…

JavaWeb系列十七: jQuery选择器 上

jQuery选择器 jQuery基本选择器jquery层次选择器基础过滤选择器内容过滤选择器可见度过滤选择器 选择器是jQuery的核心, 在jQuery中, 对事件处理, 遍历 DOM和Ajax 操作都依赖于选择器jQuery选择器的优点 $(“#id”) 等价于 document.getElementById(“id”);$(“tagName”) 等价…

Anzo Capital昂首资本独家揭秘,掌握价格行为交易法则,轻松盈利

探索交易成功的秘密!Anzo Capital昂首资本独家揭秘价格行为模式的五大核心步骤&#xff0c;助各位投资者都能把握市场脉搏&#xff0c;轻松盈利。 第一步&#xff0c;精准识别市场趋势&#xff0c;为成功交易奠定坚实基础。 第二步&#xff0c;洞察图表密码&#xff0c;巧妙标…

程序员系统入门大模型的路径和资源,看这篇就够了

本篇文章面向对大模型领域感兴趣&#xff0c;又不知如何下嘴的程序员。 看一下围绕大模型的应用场景和人才需求&#xff1a; **Prompt工程&#xff1a;**基于提示词对大模型的使用&#xff0c;会问问题就行。 **基于大模型的应用&#xff08;狭义的&#xff09;&#xff1a;*…

Avalonia 常用控件二 Menu相关

1、Menu 添加代码如下 <Button HorizontalAlignment"Center" Content"Menu/菜单"><Button.Flyout><MenuFlyout><MenuItem Header"打开"/><MenuItem Header"-"/><MenuItem Header"关闭"/&…

一文讲清楚人工智能集成学习之多模型投票(Voting)

一、集成学习 集成学习是人工智能领域中一种强大的机器学习方法&#xff0c;它通过结合多个学习器来提高整体的预测或分类性能&#xff0c;通常能够比单一模型表现得更好。 1.1 集成学习的原理 集成学习的核心思想是“集思广益”&#xff0c;即通过集合多个模型的预测结果来提…

面向对象修炼手册(二)(消息与继承)(Java宝典)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;面向对象修炼手册 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 前言 消息传递 1 基本概念 1.…

Python19 lambda表达式

在 Python 中&#xff0c;lambda 表达式是一个小型匿名函数&#xff0c;通常用于实现简单、单行的函数。lambda 函数可以接受任意数量的参数&#xff0c;但只能有一个表达式。 基本语法&#xff1a; lambda arguments: expression这里&#xff0c;arguments 是传递给 lambda …

LeetCode —— 只出现一次的数字

只出现一次的数字 I 本题依靠异或运算符的特性&#xff0c;两个相同数据异或等于0&#xff0c;数字与0异或为本身即可解答。代码如下: class Solution { public:int singleNumber(vector<int>& nums) {int ret 0;for (auto e : nums){ret ^ e;}return ret;} };只出…

Kubernetes排错(十)-处理容器数据磁盘被写满

容器数据磁盘被写满造成的危害: 不能创建 Pod (一直 ContainerCreating)不能删除 Pod (一直 Terminating)无法 exec 到容器 如何判断是否被写满&#xff1f; 容器数据目录大多会单独挂数据盘&#xff0c;路径一般是 /var/lib/docker&#xff0c;也可能是 /data/docker 或 /o…

基于CDMA的多用户水下无线光通信(3)——解相关多用户检测

继续上一篇博文&#xff0c;本文将介绍基于解相关的多用户检测算法。解相关检测器的优点是因不需要估计各个用户的接收信号幅值而具有抗远近效应的能力。常规的解相关检测器有运算量大和实时性差的缺点&#xff0c;本文针对异步CDMA的MAI主要来自干扰用户的相邻三个比特周期的特…