OrdinaryFolk
OrdinaryFolk
  • 发布:2025-11-11 09:40
  • 更新:2025-11-11 09:40
  • 阅读:15

【报Bug】video 组件 @loadedmetadata事件不触发 @longpress事件 全屏无效

分类:uni-app

产品分类: uniapp/App

PC开发环境操作系统: Windows

PC开发环境操作系统版本号: Windows11

HBuilderX类型: 正式

HBuilderX版本号: 4.85

手机系统: iOS

手机系统版本号: iOS 16

手机厂商: 苹果

手机机型: iPhoneX

页面类型: nvue

vue版本: vue3

打包方式: 云端

项目创建方式: HBuilderX

示例代码:
<template>  
    <view style="flex: auto">  
        <!-- :style="{ width: `${videoWidth}rpx`, height: `${videoHeight}rpx` }" -->  
        <video  
            id="video"  
            src="https://ygg-all.oss-cn-zhangjiakou.aliyuncs.com/app/video/xuanchuan.mp4"  
            class="relative"  
            :style="{ width: `${videoWidth}rpx`, height: `${videoHeight}rpx` }"  
            :controls="false"  
            :enable-play-gesture="true"  
            :autoplay="false"  
            :vslide-gesture-in-fullscreen="false"  
            :advanced="advanced"  
            @controlstoggle="onControlstoggle"  
            @loadedmetadata="loadedmetadata"  
            @fullscreenchange="onFullscreenChange"  
            @play="isPlay = true"  
            @pause="isPlay = false"  
            @ended="isPlay = false"  
            @timeupdate="onTimeUpdate"  
            @longpress="setPlaybackRate(2)"  
            @touchend="setPlaybackRate(1)"  
            @fullscreenclick="test"  
        >  
            <view class="controls-head" :style="{ transform: `translateY(${showControls ? 0 : '-100%'})`, position: isFullScreen ? 'fixed' : 'absolute' }">  
                <text class="controls-head-title" @longpress="onClick">标题</text>  

                <view class="controls-head-capsules" @tap="showMenu = !showMenu">  
                    <text @longpress="onClick" class="controls-head-capsules-text" style="color: white">章节</text>  
                    <text class="controls-head-capsules-icon">&#xe655;</text>  
                </view>  
            </view>  
            <view class="controls-bar" @tap.stop.prevent :style="{ transform: `translateY(${showControls ? 0 : '100%'})`, position: isFullScreen ? 'fixed' : 'absolute' }">  
                <text class="controls-bar-play" @tap="onPlay">{{ isPlay ? '&#xe64b;' : '&#xe63b;' }}</text>  

                <text class="controls-bar-current-time">{{ formatTime(videoCurrentTime) }}</text>  

                <view style="flex: 1" @tap.stop.prevent>  
                    <slider  
                        :value="videoCurrentTime"  
                        :min="0"  
                        :max="videoDuration"  
                        :step="1"  
                        :block-size="10"  
                        @changing="handerSilderChanging($event.detail.value)"  
                        @change="handerSilderChange($event.detail.value)"  
                    />  
                </view>  

                <text class="controls-bar-duration">{{ formatTime(videoDuration) }}</text>  

                <view class="controls-bar-rate" @tap="showRates = !showRates">  
                    <text class="controls-bar-rate-number">{{ currentRate.toFixed(1) }}</text>  
                    <text class="controls-bar-rate-text">倍速</text>  
                </view>  

                <text class="controls-bar-fullscreen" @tap.stop="onFullScreen()">{{ isFullScreen ? '&#xe629;' : '&#xe600;' }}</text>  
            </view>  

            <view  
                class="controls-bar-rate-list"  
                :style="{  
                    transform: `translateX(${showControls && showRates ? 0 : '200%'})`,  
                    opacity: showControls && showRates ? 1 : 0,  
                    position: isFullScreen ? 'fixed' : 'absolute'  
                }"  
            >  
                <text class="controls-bar-rate-list-number" v-for="(item, index) in rates" :key="index" :class="{ avtion: item === currentRate }" @tap="setPlaybackRate(item)">  
                    {{ item.toFixed(1) }}  
                </text>  
            </view>  
            <view  
                class="menu-list"  
                :style="{  
                    transform: `translateX(${showControls && showMenu ? 0 : '200%'})`,  
                    opacity: showControls && showMenu ? 1 : 0,  
                    position: isFullScreen ? 'fixed' : 'absolute'  
                }"  
            >  
                <list :show-scrollbar="false">  
                    <cell v-for="i in 10" :key="i">  
                        <view class="menu-list-item">  
                            <text class="menu-list-item-text">知识点{{ i }}:xxxx</text>  
                            <!-- &#xe63b; -->  
                            <text class="menu-list-item-icon">&#xe64b;</text>  
                        </view>  
                    </cell>  
                </list>  
            </view>  
        </video>  
    </view>  
</template>  

<script setup>  
import { ref, onBeforeMount, watch } from 'vue';  
import { onReady } from '@dcloudio/uni-app';  
const advanced = ref([  
    {  
        key: 'auto_convert',  
        value: 1,  
        type: 'format'  
    }  
]);  
const video = ref(null);  
const videoWidth = ref(750);  
const videoHeight = ref(421);  
const videoDuration = ref(0);  
const videoCurrentTime = ref(0);  

const isFullScreen = ref(false);  
const isPlay = ref(false);  

const rates = ref([0.5, 1, 1.5, 2]);  
const currentRate = ref(1);  
const showRates = ref(false);  
const showControls = ref(false);  
const showMenu = ref(false);  

const changing = ref(false);  

const handerSilderChanging = (v) => {  
    changing.value = true;  
    videoCurrentTime.value = v;  
};  
const handerSilderChange = (v) => {  
    changing.value = false;  
    video.value.seek(v);  
};  
const onControlstoggle = ({ detail }) => {  
    showControls.value = detail.show;  
};  

const onClick = (e) => {  
    console.log(e);  
};  
const setPlaybackRate = (rate) => {  
    video.value.playbackRate(rate);  
    currentRate.value = rate;  
    showRates.value = false;  
};  
const loadedmetadata = ({ detail }) => {  
    console.log(detail);  
    const { width, height, duration } = detail;  

    videoWidth.value = width;  
    videoHeight.value = height;  
    videoDuration.value = duration;  
};  
const onPlay = () => {  
    if (isPlay.value) {  
        video.value.pause();  
    } else {  
        video.value.play();  
    }  
};  
const onFullscreenChange = (e) => {  
    const { fullScreen } = e.detail;  
    isFullScreen.value = fullScreen;  
};  
const onFullScreen = (direction = 'horizontal') => {  
    if (isFullScreen.value) {  
        video.value.exitFullScreen();  
    } else {  
        video.value.requestFullScreen();  
    }  
};  

const onTimeUpdate = ({ detail }) => {  
    const { currentTime, duration } = detail;  

    if (changing.value) return;  

    videoCurrentTime.value = currentTime;  
    videoDuration.value = duration;  
};  
const test = (e) => {  
    console.log(e);  
    // const { screenWidth, screenHeight } = e.detail;  
    // videoWidth.value = screenWidth;  
    // videoHeight.value = screenHeight;  
};  

const formatTime = (seconds) => {  
    if (isNaN(seconds) || seconds < 0) return '00:00';  

    seconds = Math.floor(seconds);  
    const h = Math.floor(seconds / 3600);  
    const m = Math.floor((seconds % 3600) / 60);  
    const s = seconds % 60;  

    if (h > 0) {  
        // 大于 1 小时 → 00:00:00  
        return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;  
    } else {  
        // 小于 1 小时 → 00:00  
        return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;  
    }  
};  

watch(  
    () => ({  
        controls: showControls.value,  
        fullscreen: isFullScreen.value  
    }),  
    (newVal, oldVal) => {  
        showRates.value = false;  
        showMenu.value = false;  
    }  
);  
onBeforeMount(() => {  
    // #ifdef APP-PLUS-NVUE  

    const domModule = uni.requireNativePlugin('dom');  
    domModule.addRule('fontFace', {  
        fontFamily: 'iconfont',  
        src: "url('/static/font/iconfont.ttf')"  
    });  
    // #endif  
});  
onReady(() => {  
    video.value = uni.createVideoContext('video');  
});  
</script>  

<style scoped lang="scss">  
$themeColor: #2150ec;  
.controls-head {  
    position: absolute;  
    top: 0;  
    left: 0;  
    right: 0;  
    flex-direction: row;  
    justify-content: space-between;  
    padding: 10rpx 30rpx;  
    background-image: linear-gradient(to bottom, black, transparent);  

    transition-property: transform, background-color;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    &-title {  
        color: white;  
        font-size: 14px;  
    }  
    &-capsules {  
        flex-direction: row;  
        align-items: center;  
        justify-content: center;  
        &-text {  
            color: white;  
            font-size: 12px;  
        }  
        &-icon {  
            font-family: iconfont;  
            font-size: 30rpx;  
            background-color: $themeColor;  
            border-radius: 999px;  
            margin-left: 4px;  
            text-align: center;  
            width: 36rpx;  
            height: 36rpx;  
            line-height: 36rpx;  
        }  
    }  
}  

.controls-bar {  
    position: absolute;  
    bottom: 0;  
    left: 0;  
    right: 0;  
    flex-direction: row;  
    justify-content: space-between;  
    align-items: center;  
    padding: 10rpx 30rpx;  
    background-image: linear-gradient(to top, black, transparent);  

    transition-property: transform, background-color;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    &-play {  
        font-family: iconfont;  
        font-size: 50rpx;  
        color: $themeColor;  
    }  

    &-current-time,  
    &-duration {  
        color: white;  
        font-size: 14px;  
        padding: 0 20rpx;  
    }  
    &-rate {  
        flex-direction: row;  
        background-color: $themeColor;  
        border-radius: 999px;  
        padding: 2px;  
        margin-right: 20rpx;  
        position: relative;  
        &-list {  
            position: absolute;  
            bottom: 70rpx;  
            right: 90rpx;  
            background-color: rgba(255, 255, 255, 0.5);  
            width: 100rpx;  
            border-radius: 10rpx;  

            transition-property: transform, opacity;  
            transition-duration: 0.3s;  
            transition-timing-function: linear;  

            // opacity: 0;  
            // transform: translateY(150rpx);  

            &-number {  
                text-align: center;  
                border-radius: 10rpx;  

                &.avtion {  
                    background-color: #2150ec;  
                }  
            }  
        }  
        &-number {  
            width: 36rpx;  
            height: 36rpx;  
            border-radius: 999px;  
            background-color: black;  
            color: $themeColor;  
            text-align: center;  
            line-height: 36rpx;  
            font-size: 12px;  
        }  
        &-text {  
            color: white;  
            font-size: 12px;  
            line-height: 36rpx;  
            padding: 0 8rpx;  
        }  
    }  
    &-fullscreen {  
        font-family: iconfont;  
        font-size: 24rpx;  
        width: 36rpx;  
        height: 36rpx;  
        border-radius: 999px;  
        background-color: $themeColor;  
        text-align: center;  
        line-height: 36rpx;  
    }  
}  

.menu-list {  
    position: absolute;  
    right: 30rpx;  
    top: 60rpx;  
    bottom: 70rpx;  
    background-color: #333333;  
    border-radius: 20rpx;  
    flex-direction: column;  
    padding: 10rpx 20rpx;  
    transition-property: transform, opacity;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    // transform: translateX(200%);  
    // opacity: 0;  

    &-item {  
        margin-bottom: 10rpx;  
        flex-direction: row;  
        justify-content: space-between;  
        align-items: center;  
        &-text {  
            color: white;  
            font-size: 12px;  
        }  

        &-icon {  
            margin-left: 30rpx;  
            font-family: iconfont;  
            color: $themeColor;  
            font-size: 12px;  
        }  
    }  
}  
</style>

操作步骤:
<template>  
    <view style="flex: auto">  
        <!-- :style="{ width: `${videoWidth}rpx`, height: `${videoHeight}rpx` }" -->  
        <video  
            id="video"  
            src="https://ygg-all.oss-cn-zhangjiakou.aliyuncs.com/app/video/xuanchuan.mp4"  
            class="relative"  
            :style="{ width: `${videoWidth}rpx`, height: `${videoHeight}rpx` }"  
            :controls="false"  
            :enable-play-gesture="true"  
            :autoplay="false"  
            :vslide-gesture-in-fullscreen="false"  
            :advanced="advanced"  
            @controlstoggle="onControlstoggle"  
            @loadedmetadata="loadedmetadata"  
            @fullscreenchange="onFullscreenChange"  
            @play="isPlay = true"  
            @pause="isPlay = false"  
            @ended="isPlay = false"  
            @timeupdate="onTimeUpdate"  
            @longpress="setPlaybackRate(2)"  
            @touchend="setPlaybackRate(1)"  
            @fullscreenclick="test"  
        >  
            <view class="controls-head" :style="{ transform: `translateY(${showControls ? 0 : '-100%'})`, position: isFullScreen ? 'fixed' : 'absolute' }">  
                <text class="controls-head-title" @longpress="onClick">标题</text>  

                <view class="controls-head-capsules" @tap="showMenu = !showMenu">  
                    <text @longpress="onClick" class="controls-head-capsules-text" style="color: white">章节</text>  
                    <text class="controls-head-capsules-icon">&#xe655;</text>  
                </view>  
            </view>  
            <view class="controls-bar" @tap.stop.prevent :style="{ transform: `translateY(${showControls ? 0 : '100%'})`, position: isFullScreen ? 'fixed' : 'absolute' }">  
                <text class="controls-bar-play" @tap="onPlay">{{ isPlay ? '&#xe64b;' : '&#xe63b;' }}</text>  

                <text class="controls-bar-current-time">{{ formatTime(videoCurrentTime) }}</text>  

                <view style="flex: 1" @tap.stop.prevent>  
                    <slider  
                        :value="videoCurrentTime"  
                        :min="0"  
                        :max="videoDuration"  
                        :step="1"  
                        :block-size="10"  
                        @changing="handerSilderChanging($event.detail.value)"  
                        @change="handerSilderChange($event.detail.value)"  
                    />  
                </view>  

                <text class="controls-bar-duration">{{ formatTime(videoDuration) }}</text>  

                <view class="controls-bar-rate" @tap="showRates = !showRates">  
                    <text class="controls-bar-rate-number">{{ currentRate.toFixed(1) }}</text>  
                    <text class="controls-bar-rate-text">倍速</text>  
                </view>  

                <text class="controls-bar-fullscreen" @tap.stop="onFullScreen()">{{ isFullScreen ? '&#xe629;' : '&#xe600;' }}</text>  
            </view>  

            <view  
                class="controls-bar-rate-list"  
                :style="{  
                    transform: `translateX(${showControls && showRates ? 0 : '200%'})`,  
                    opacity: showControls && showRates ? 1 : 0,  
                    position: isFullScreen ? 'fixed' : 'absolute'  
                }"  
            >  
                <text class="controls-bar-rate-list-number" v-for="(item, index) in rates" :key="index" :class="{ avtion: item === currentRate }" @tap="setPlaybackRate(item)">  
                    {{ item.toFixed(1) }}  
                </text>  
            </view>  
            <view  
                class="menu-list"  
                :style="{  
                    transform: `translateX(${showControls && showMenu ? 0 : '200%'})`,  
                    opacity: showControls && showMenu ? 1 : 0,  
                    position: isFullScreen ? 'fixed' : 'absolute'  
                }"  
            >  
                <list :show-scrollbar="false">  
                    <cell v-for="i in 10" :key="i">  
                        <view class="menu-list-item">  
                            <text class="menu-list-item-text">知识点{{ i }}:xxxx</text>  
                            <!-- &#xe63b; -->  
                            <text class="menu-list-item-icon">&#xe64b;</text>  
                        </view>  
                    </cell>  
                </list>  
            </view>  
        </video>  
    </view>  
</template>  

<script setup>  
import { ref, onBeforeMount, watch } from 'vue';  
import { onReady } from '@dcloudio/uni-app';  
const advanced = ref([  
    {  
        key: 'auto_convert',  
        value: 1,  
        type: 'format'  
    }  
]);  
const video = ref(null);  
const videoWidth = ref(750);  
const videoHeight = ref(421);  
const videoDuration = ref(0);  
const videoCurrentTime = ref(0);  

const isFullScreen = ref(false);  
const isPlay = ref(false);  

const rates = ref([0.5, 1, 1.5, 2]);  
const currentRate = ref(1);  
const showRates = ref(false);  
const showControls = ref(false);  
const showMenu = ref(false);  

const changing = ref(false);  

const handerSilderChanging = (v) => {  
    changing.value = true;  
    videoCurrentTime.value = v;  
};  
const handerSilderChange = (v) => {  
    changing.value = false;  
    video.value.seek(v);  
};  
const onControlstoggle = ({ detail }) => {  
    showControls.value = detail.show;  
};  

const onClick = (e) => {  
    console.log(e);  
};  
const setPlaybackRate = (rate) => {  
    video.value.playbackRate(rate);  
    currentRate.value = rate;  
    showRates.value = false;  
};  
const loadedmetadata = ({ detail }) => {  
    console.log(detail);  
    const { width, height, duration } = detail;  

    videoWidth.value = width;  
    videoHeight.value = height;  
    videoDuration.value = duration;  
};  
const onPlay = () => {  
    if (isPlay.value) {  
        video.value.pause();  
    } else {  
        video.value.play();  
    }  
};  
const onFullscreenChange = (e) => {  
    const { fullScreen } = e.detail;  
    isFullScreen.value = fullScreen;  
};  
const onFullScreen = (direction = 'horizontal') => {  
    if (isFullScreen.value) {  
        video.value.exitFullScreen();  
    } else {  
        video.value.requestFullScreen();  
    }  
};  

const onTimeUpdate = ({ detail }) => {  
    const { currentTime, duration } = detail;  

    if (changing.value) return;  

    videoCurrentTime.value = currentTime;  
    videoDuration.value = duration;  
};  
const test = (e) => {  
    console.log(e);  
    // const { screenWidth, screenHeight } = e.detail;  
    // videoWidth.value = screenWidth;  
    // videoHeight.value = screenHeight;  
};  

const formatTime = (seconds) => {  
    if (isNaN(seconds) || seconds < 0) return '00:00';  

    seconds = Math.floor(seconds);  
    const h = Math.floor(seconds / 3600);  
    const m = Math.floor((seconds % 3600) / 60);  
    const s = seconds % 60;  

    if (h > 0) {  
        // 大于 1 小时 → 00:00:00  
        return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;  
    } else {  
        // 小于 1 小时 → 00:00  
        return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;  
    }  
};  

watch(  
    () => ({  
        controls: showControls.value,  
        fullscreen: isFullScreen.value  
    }),  
    (newVal, oldVal) => {  
        showRates.value = false;  
        showMenu.value = false;  
    }  
);  
onBeforeMount(() => {  
    // #ifdef APP-PLUS-NVUE  

    const domModule = uni.requireNativePlugin('dom');  
    domModule.addRule('fontFace', {  
        fontFamily: 'iconfont',  
        src: "url('/static/font/iconfont.ttf')"  
    });  
    // #endif  
});  
onReady(() => {  
    video.value = uni.createVideoContext('video');  
});  
</script>  

<style scoped lang="scss">  
$themeColor: #2150ec;  
.controls-head {  
    position: absolute;  
    top: 0;  
    left: 0;  
    right: 0;  
    flex-direction: row;  
    justify-content: space-between;  
    padding: 10rpx 30rpx;  
    background-image: linear-gradient(to bottom, black, transparent);  

    transition-property: transform, background-color;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    &-title {  
        color: white;  
        font-size: 14px;  
    }  
    &-capsules {  
        flex-direction: row;  
        align-items: center;  
        justify-content: center;  
        &-text {  
            color: white;  
            font-size: 12px;  
        }  
        &-icon {  
            font-family: iconfont;  
            font-size: 30rpx;  
            background-color: $themeColor;  
            border-radius: 999px;  
            margin-left: 4px;  
            text-align: center;  
            width: 36rpx;  
            height: 36rpx;  
            line-height: 36rpx;  
        }  
    }  
}  

.controls-bar {  
    position: absolute;  
    bottom: 0;  
    left: 0;  
    right: 0;  
    flex-direction: row;  
    justify-content: space-between;  
    align-items: center;  
    padding: 10rpx 30rpx;  
    background-image: linear-gradient(to top, black, transparent);  

    transition-property: transform, background-color;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    &-play {  
        font-family: iconfont;  
        font-size: 50rpx;  
        color: $themeColor;  
    }  

    &-current-time,  
    &-duration {  
        color: white;  
        font-size: 14px;  
        padding: 0 20rpx;  
    }  
    &-rate {  
        flex-direction: row;  
        background-color: $themeColor;  
        border-radius: 999px;  
        padding: 2px;  
        margin-right: 20rpx;  
        position: relative;  
        &-list {  
            position: absolute;  
            bottom: 70rpx;  
            right: 90rpx;  
            background-color: rgba(255, 255, 255, 0.5);  
            width: 100rpx;  
            border-radius: 10rpx;  

            transition-property: transform, opacity;  
            transition-duration: 0.3s;  
            transition-timing-function: linear;  

            // opacity: 0;  
            // transform: translateY(150rpx);  

            &-number {  
                text-align: center;  
                border-radius: 10rpx;  

                &.avtion {  
                    background-color: #2150ec;  
                }  
            }  
        }  
        &-number {  
            width: 36rpx;  
            height: 36rpx;  
            border-radius: 999px;  
            background-color: black;  
            color: $themeColor;  
            text-align: center;  
            line-height: 36rpx;  
            font-size: 12px;  
        }  
        &-text {  
            color: white;  
            font-size: 12px;  
            line-height: 36rpx;  
            padding: 0 8rpx;  
        }  
    }  
    &-fullscreen {  
        font-family: iconfont;  
        font-size: 24rpx;  
        width: 36rpx;  
        height: 36rpx;  
        border-radius: 999px;  
        background-color: $themeColor;  
        text-align: center;  
        line-height: 36rpx;  
    }  
}  

.menu-list {  
    position: absolute;  
    right: 30rpx;  
    top: 60rpx;  
    bottom: 70rpx;  
    background-color: #333333;  
    border-radius: 20rpx;  
    flex-direction: column;  
    padding: 10rpx 20rpx;  
    transition-property: transform, opacity;  
    transition-duration: 0.3s;  
    transition-timing-function: linear;  

    // transform: translateX(200%);  
    // opacity: 0;  

    &-item {  
        margin-bottom: 10rpx;  
        flex-direction: row;  
        justify-content: space-between;  
        align-items: center;  
        &-text {  
            color: white;  
            font-size: 12px;  
        }  

        &-icon {  
            margin-left: 30rpx;  
            font-family: iconfont;  
            color: $themeColor;  
            font-size: 12px;  
        }  
    }  
}  
</style>

预期结果:

@longpress事件 全屏的时候也生效,能实现长按倍数

在自定义ui,使用slider 组件当进度条 iOS 触摸乱跳触碰就直接到最大值了

实际结果:

@loadedmetadata事件不触发
@longpress事件 全屏无效
自定义ui,slider组件触摸乱跳

bug描述:

video 组件

@loadedmetadata事件不触发
@longpress事件 全屏无效
自定义ui,slider组件触摸乱跳

2025-11-11 09:40 负责人:无 分享
已邀请:

要回复问题请先登录注册