flowoverstack
flowoverstack
  • 发布:2025-11-01 09:09
  • 更新:2025-11-07 16:26
  • 阅读:104

【急急急】APP下NVUE使用liver-pusher预览和快照的图片不一致

分类:uni-app

产品分类: uniapp/App

PC开发环境操作系统: Windows

PC开发环境操作系统版本号: win10 22H2 19045.5131

HBuilderX类型: 正式

HBuilderX版本号: 4.76

手机系统: Android

手机系统版本号: Android 15

手机厂商: OPPO

手机机型: oppo-findx8u

页面类型: nvue

vue版本: vue3

打包方式: 云端

项目创建方式: HBuilderX

示例代码:
<template>  
    <!-- <view class="container"> -->  
        <live-pusher :id="livePusherId" ref="livePusher" class="live-pusher" :url="config.url" :mode="config.quality"  
            :muted="config.isMute" :enable-camera="true" :auto-focus="config.autoFocus" :beauty="config.beauty"  
            :whiteness="config.white" :aspect="config.ratio" :device-position="config.camera" @statechange="statechange">  
        </live-pusher>  
    <!-- </view> -->  

    <view class="overlay">  
        <!-- 顶部遮罩 -->  
        <view class="mask top" :style="{   
            position: 'absolute',   
            top: '0px',   
            left: '0px',   
            height:  highlightTop + 'px',   
            width:  systemInfo.screenWidth + 'px',  
            backgroundColor: 'rgba(0,0,0,0.7)'   
        }">  
        </view>  

        <!-- 底部遮罩 -->  
        <view class="mask bottom" :style="{   
            position: 'absolute',   
            top: highlightTop +highlightHeight+ 'px',   
            left: '0px',    
            height: bottomMaskHeight + 'px',   
            width:  systemInfo.screenWidth + 'px',  
            backgroundColor: 'rgba(0,0,0,0.7)'   
        }">  
        </view>  

        <!-- 左侧遮罩 -->  
        <view class="mask left" :style="{   
            position: 'absolute',   
            top: leftMaskTop + 'px',   
            left: '0px',   
            width: leftMaskWidth + 'px',   
            height: highlightHeight + 'px',   
            backgroundColor: 'rgba(0,0,0,0.7)'   
        }">  
        </view>  

        <!-- 右侧遮罩 -->  
        <view class="mask right" :style="{   
             position: 'absolute',   
             top: leftMaskTop + 'px',   
             left: (highlightLeft + highlightWidth) + 'px',   
             width: leftMaskWidth + 'px',   
             height: highlightHeight + 'px',   
             backgroundColor: 'rgba(0,0,0,0.7)'   
         }">  
        </view>  

        <!-- 高亮边框 -->  
        <view class="highlight-frame" :style="{   
            position: 'absolute',   
            top: highlightTop + 'px',   
            left: highlightLeft + 'px',   
            width: highlightWidth + 'px',   
            height: highlightHeight + 'px',   
            border: '2px solid #008CFF'   
        }">  
            <view class="scan-line" :style="{   
                position: 'absolute',   
                top: scanTop + 'px',   
                left: '0px',   
                right: '0px',   
                height: '2px',   
                backgroundColor: '#008CFF'   
            }">  
            </view>  
            <view class="corner tl"></view>  
            <view class="corner tr"></view>  
            <view class="corner bl"></view>  
            <view class="corner br"></view>  
        </view>  
        <text class="tip-text" :style="{top: (highlightTop + highlightHeight + 10)+'px'}">{{tipText}}</text>  
    </view>  

</template>  

<script setup>  
    import {  
        onLoad,  
        onUnload,  
        onHide  
    } from "@dcloudio/uni-app";  
    import {  
        ref,  
        getCurrentInstance,  
        nextTick,  
        onMounted  
    } from "vue";  

    import {  
        useDetectionStore  
    } from '../../store/modules/detection'  

    const userDetectionStore = useDetectionStore()  

    const systemInfo = uni.getSystemInfoSync();  
const pusherComponentSize = ref({ width: 0, height: 0 });  

    // 获取当前设备宽、高  
    const screen = {  
        width: 375,  
        height: 667,  
    }  
    // 动态计算遮罩层的宽高和位置  
    const bottomMaskHeight = ref(0);  
    const leftMaskWidth = ref(0);  
    const highlightWidth = ref(0);  
    const highlightHeight = ref(0);  
    const highlightTop = ref(0);  
    const highlightLeft = ref(0);  
    const leftMaskTop = ref(0);  

    // livePusher相关申明  
    const livePusherId = ref(null);  
    const livePusher = ref(null);  
    const livePusherContext = ref(null);  

    // livePusher 配置  
    const config = ref({  
        url: '',  
        quality: 'SD',  
        ratio: '9:16',  
        isMute: false,  
        autoFocus: true,  
        beauty: 0,  
        white: 0,  
        camera: 'back'  
    })  
    // 添加扫描线位置变量  
    const scanTop = ref(0);  
    // 扫描线定时器  
    let scanTimer = null;  
    // 定时拍摄  
    let shotTimer = null;  
    // 文本提示  
    const tipText = ref('');  

    // 切换预览状态  
    const togglePreview = (target) => {  

        if (!livePusherContext.value) {  

            return;  
        }  

        if (target === 'stop') {  
            // 停止预览  
            livePusherContext.value.stopPreview({  
                success: () => {  

                },  
                fail: (err) => {  
                    uni.showModal({  
                        title: '停止预览失败',  
                        content: err.message || err.errMsg,  
                        showCancel: false  
                    });  
                }  
            });  
        }  

        if (target === 'start') {  
            // 开始预览  
            livePusherContext.value.startPreview({  
                success: () => {  
                    startScanAnimation();  
                },  
                fail: (err) => {  
                    uni.showModal({  
                        title: '预览开启失败',  
                        content: err.message || err.errMsg,  
                        showCancel: false  
                    });  
                }  
            });  
        }  
    }  

    // 扫描线动画  
    const startScanAnimation = () => {  
        stopScanAnimation();  
        let top = 0;  
        const frameHeight = systemInfo.windowHeight * 0.5; // 高亮框高度为窗口的50%  
        scanTimer = setInterval(() => {  
            top += 2;  
            if (top > frameHeight) {  
                top = 0;  
            }  
            scanTop.value = top;  
        }, 30);  
    }  

    // 将快照发送给服务器  
    const takeScreenshot = async () => {  
        return new Promise((resolve, reject) => {  
            livePusherContext.value.snapshot({  
                success: async (res) => {  

               // 1. 获取快照图片的真实尺寸 (图片坐标系)  
                    const imageInfo = await uni.getImageInfo({ src: res.message.tempImagePath });  
                    const { width: imgWidth, height: imgHeight } = imageInfo;  
                    console.log('--- [Step 1] 快照图片尺寸 (px):', { imgWidth, imgHeight });  

                    // 2. 获取屏幕/组件的尺寸 (屏幕坐标系)  
                    const { screenWidth, windowHeight } = systemInfo;  
                    console.log('--- [Step 2] 预览窗口尺寸 (px):', { screenWidth, windowHeight });  

                    // 3. 【核心】基于“宽度填满,上下裁剪”的模型进行计算  
                    // a. 计算视频流在屏幕上的缩放比例  
                    const scale = screenWidth / imgWidth;  
                    console.log('--- [Step 3a] 缩放比例 (screen/img):', scale);  

                    // b. 计算视频流按此比例缩放后,在屏幕上的总高度  
                    const scaledImgHeight = imgHeight * scale;  
                    console.log('--- [Step 3b] 缩放后的视频高度 (px):', scaledImgHeight);  

                    // c. 计算视频流顶部被裁剪掉的高度 (换算回图片坐标系)  
                    // 这就是“图片偏下”的根本原因  
                    const offsetY_img = (scaledImgHeight - windowHeight) / 2 / scale;  
                    console.log('--- [Step 3c] 图片顶部被裁剪的高度 (px):', offsetY_img);  

                    // 4. 【核心】坐标转换  
                    const boxX_screen = highlightLeft.value;  
                    const boxY_screen = highlightTop.value;  
                    const boxW_screen = highlightWidth.value;  
                    const boxH_screen = highlightHeight.value;  
                    console.log('--- [Step 4] 高亮框屏幕坐标 (px):', { boxX_screen, boxY_screen, boxW_screen, boxH_screen });  

                    const cropData = {  
                        // X坐标没有偏移,直接按比例缩放  
                        crop_x: Math.round(boxX_screen / scale),  
                        // Y坐标需要加上被裁剪掉的顶部偏移量  
                        crop_y: Math.round(boxY_screen / scale + offsetY_img),  
                        crop_width: Math.round(boxW_screen / scale),  
                        crop_height: Math.round(boxH_screen / scale),  
                    };  

                    console.log('--- [Step 5] 最终发送给后端的裁剪数据:', cropData);  

                    const resData = await userDetectionStore.uploadDetecionImg(res.message  
                        .tempImagePath);  
                    if (resData?.code === 200) {  
                        uni.redirectTo({  
                            url: '/pages/data/detailsData'  
                        });  
                    } else {  
                        tipText.value = resData?.msg;  
                    }  
                },  
                fail: (err) => {  
                    reject(err.message || err.errMsg);  
                }  
            });  
        });  
    };  

    const autoCaptureProcess = async () => {  
        const imgPath = await takeScreenshot();  
    };  

    // 快照  
    const takeSnapshot = () => {  
        shotTimer = setInterval(takeScreenshot, 1000);  
    }  

    // livePusher状态改变  
    const statechange = (e) => {  
        const errCode = e.detail.code || e.detail.errCode;  
        const errMsg = e.detail.message || e.detail.errMsg;  

        if (errCode == 1007) {  
            takeSnapshot();  
        }  
    }  

    // 初始化  
    const createLivePusher = () => {  
        livePusherId.value = `livePusher_${Date.now()}`;  

        if (livePusherContext.value) {  
            livePusherContext.value = null;  
        };  

        // 动态计算遮罩层的宽高和位置  
        const screenWidth = systemInfo.screenWidth;  
        const screenHeight = systemInfo.screenHeight;  

        console.log(" screenWidth " + screenWidth);  
        console.log(" screenHeight " + screenHeight);  

        // 高亮框的宽高和位置  
        highlightWidth.value = screenWidth * 0.3; // 占屏幕宽度的45%  
        highlightHeight.value = screenHeight * 0.4; // 占屏幕高度的50%  
        highlightTop.value = screenHeight * 0.3; // 距离顶部25%  
        highlightLeft.value = screenWidth * 0.35; // 距离左侧30%  

        // console.log(" screenWidth " + screenWidth);  
        // console.log(" screenHeight " + screenHeight);  
        // console.log(" highlightWidth " + highlightWidth.value);  
        // console.log(" highlightHeight " + highlightHeight.value);  
        // console.log(" highlightTop " + highlightTop.value);  
        // console.log(" highlightLeft " + highlightLeft.value);  

        // 底部遮罩:高度 = 屏幕总高度 - 高亮区域顶部 - 高亮区域高度  
        bottomMaskHeight.value = screenHeight - highlightTop.value - highlightHeight.value;  

        // 左侧遮罩:宽度 = 高亮区域左侧位置  
        leftMaskWidth.value = highlightLeft.value;  
        // 左侧遮罩层的顶部位置与高亮区域顶部对齐  
        leftMaskTop.value = highlightTop.value;  

        nextTick(() => {  
            const currentPage = getCurrentPages()[getCurrentPages().length - 1];  
            livePusherContext.value = uni.createLivePusherContext(livePusherId.value, currentPage.$vm);  
            console.log('Live-pusher 组件尺寸:', JSON.stringify(livePusherContext.value))  
            // const query = uni.createSelectorQuery().in(currentPage);  
            // query.select('.live-pusher').boundingClientRect(data => {  
            //     if (data) {  
            //         pusherComponentSize.value = { width: data.width, height: data.height };  
            //         console.log('Live-pusher 组件尺寸:', pusherComponentSize.value);  
            //     }  
            // }).exec();  

            setTimeout(togglePreview.bind(null, 'start'), 100)  

        })  
    }  

    // 停止扫描线动画  
    const stopScanAnimation = () => {  
        if (scanTimer) {  
            clearInterval(scanTimer);  
        }  
        scanTimer = null;  
    }  

    // 停止定时快照  
    const stopAutoCapture = () => {  
        if (shotTimer) {  
            clearInterval(shotTimer)  
        }  
        shotTimer = null  
    }  

    onUnload(() => {  
        stopScanAnimation();  
        stopAutoCapture();  
    });  

    onHide(() => {  
        stopScanAnimation();  
        stopAutoCapture();  
    });  

    onLoad(() => {  
        createLivePusher();  
    });  
</script>  

<style scoped>  
    .container {  
        display: flex;  
        flex-direction: column;  
        flex: 1;  
        height: 100%;  
        background: rgba(0, 0, 0, 0.700);  
    }  

    /* 推流容器:占满根容器(确保画面全屏) */  
    .live-pusher {  
        display: flex;  
        flex-direction: column;  
        flex: 1;  
        background: rgba(0, 0, 0, 0.700);  
    }  

    /* 截图预览:保持原有 */  
    .snapshot-preview {  
        position: fixed;  
        right: 20px;  
        top: 100px;  
        z-index: 1000;  
    }  

    .snapshot-preview-image {  
        width: 100%;  
        height: auto;  
    }  

    .overlay {  
        position: absolute;  
        top: 0;  
        left: 0;  
        pointer-events: none;  
        z-index: 9999;  
        flex: 0  
    }  

    /* 半透明遮罩 */  
    .mask {  
        position: absolute;  
        background: rgba(0, 0, 0, 0.700);  
    }  

    /* 高亮边框 */  
    .highlight-frame {  
        position: absolute;  
        border: 2px solid #008CFF;  
        overflow: hidden;  
        position: relative;  
        box-shadow: 0 0 15px rgba(39, 89, 255, 0.5);  
    }  

    /* 四角装饰 */  
    .corner {  
        position: absolute;  
        width: 20px;  
        height: 20px;  
        border-style: solid;  
        border-color: #008CFF;  
        z-index: 2;  
    }  

    .tl {  
        /* 左上角 */  
        top: -2px;  
        left: -2px;  
        border-width: 2px 0 0 2px;  
    }  

    .tr {  
        /* 右上角 */  
        top: -2px;  
        right: -2px;  
        border-width: 2px 2px 0 0;  
    }  

    .bl {  
        /* 左下角 */  
        bottom: -2px;  
        left: -2px;  
        border-width: 0 0 2px 2px;  
    }  

    .br {  
        /* 右下角 */  
        bottom: -2px;  
        right: -2px;  
        border-width: 0 2px 2px 0;  
    }  

    .tip-text {  
        color: #EBEFFA;  
        font-size: 14px;  
        text-align: center;  
        position: absolute;  
        align-items: center;  
        word-wrap: break-word;  
        lines: 2;  
        width: 750rpx;  
        left: 0;  
    }  
</style>

操作步骤:

复现步骤参考代码,预览时物品在高亮框中,快照后将图片发送给后端,后端再用前端高亮框的裁剪比例裁剪图片此时物品不在高亮框中。

预期结果:

预览和快照图片需一致

实际结果:

预览和快照图片不一致

bug描述:

目前业务逻辑是采用nvue中的liver-pusher的快照功能进行定时截图然后发送给后端,

即预览时物品在高亮框中,快照后将图片发送给后端,后端再用前端高亮框的裁剪比例裁剪图片此时物品不在高亮框中调试代码见下方。

1、页面会有高亮框,用户将物品放置高亮框中进行拍照。预览时高亮框中包含物品,快照发送给后端图片后,后端用前端相同的裁剪比例对图片进行裁剪,这个时候发现图片整体偏下。

2025-11-01 09:09 负责人:DCloud_App_Array 分享
已邀请:
DCloud_UNI_OttoJi

DCloud_UNI_OttoJi - 日常回复 uni-app/x 问题,如果艾特我没看到,请主动私信

nvue 进入维护阶段了,一般不会有明显的问题。

请提供截图说明问题,预览和拍照有多大差异,是否稳定复现,判断是否有稳定的规律,是否可以绕过操作,建议定位相关规律

  • flowoverstack (作者)

    https://ask.dcloud.net.cn/question/203608 24年已经有人反馈这个问题了。这个是稳定复现和nvue没关系,和liver-pusher有关系

    2025-11-03 15:12

flowoverstack

flowoverstack (作者)

有官方的人吗?

要回复问题请先登录注册