uni-app中APP下NVUE使用liver-pusher预览和快照的图片不一致

uni-app中APP下NVUE使用liver-pusher预览和快照的图片不一致

开发环境 版本号 项目创建方式
Windows win10 22H2 19045.5131 HBuilderX
产品分类:uniapp/App

PC开发环境操作系统:Windows

HBuilderX类型:正式

HBuilderX版本号:4.76

手机系统:Android

手机系统版本号:Android 15

手机厂商:OPPO

手机机型:oppo-findx8u

页面类型:nvue

vue版本:vue3

打包方式:云端

项目创建方式:HBuilderX

### 示例代码:

```html
<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](/user/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%  

    // 底部遮罩:高度 = 屏幕总高度 - 高亮区域顶部 - 高亮区域高度  
    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))  
        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. 页面会有高亮框,用户将物品放置高亮框中进行拍照。预览时高亮框中包含物品,快照发送给后端图片后,后端用前端相同的裁剪比例对图片进行裁剪,这个时候发现图片整体偏下。

更多关于uni-app中APP下NVUE使用liver-pusher预览和快照的图片不一致的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

nvue 进入维护阶段了,一般不会有明显的问题。
请提供截图说明问题,预览和拍照有多大差异,是否稳定复现,判断是否有稳定的规律,是否可以绕过操作,建议定位相关规律

更多关于uni-app中APP下NVUE使用liver-pusher预览和快照的图片不一致的实战教程也可以访问 https://www.itying.com/category-93-b0.html


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

有官方的人吗?

这是一个典型的预览与快照坐标系不一致的问题。从你的代码分析,问题核心在于:

预览渲染与快照图片的坐标系差异

  1. 预览渲染:live-pusher组件在屏幕上渲染时,为了保持宽高比且填满宽度,会对视频流进行缩放和裁剪,导致预览画面与原始图片存在位置偏移。

  2. 快照图片snapshot()获取的是原始视频帧,未经过预览时的裁剪处理。

问题根源: 你的坐标转换计算中,虽然考虑了Y轴偏移(offsetY_img),但可能存在计算误差。特别是当视频流宽高比与屏幕宽高比差异较大时,实际的裁剪策略可能与你的假设不符。

建议排查方向

  1. 验证实际裁剪策略:通过console.log输出完整的尺寸信息,确认视频流在预览时的实际缩放和裁剪方式。

  2. 检查设备像素比:考虑pixelRatio对坐标转换的影响:

    const pixelRatio = systemInfo.pixelRatio;
    // 在坐标转换时可能需要考虑
回到顶部