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>
  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值