You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular Canvas移动端截图优化:Android设备4张PNG截图耗时过长求助

Android端Canvas连续截图性能优化问题

我在Angular项目中使用Canvas捕获4张PNG图片,通过npm run build:widgets命令将项目打包为包含HTML、CSS、JS文件的Widget,部署至S3后通过Android或iPhone的浏览器访问。在iPhone上能按预期以50毫秒的间隔完成4张图片的捕获,但Android设备捕获全部4张图片耗时超过5秒。请问如何针对Android移动端进行优化,或是我应该采用其他方案?

相关代码

HTML

<canvas class="d-none"></canvas>
<button type="button" (click)="captureImage()">
    CaptureImage
</button>

TypeScript

canvas: any = document.querySelector('canvas');

/**
 * Function to capture image
 */
async captureImage() { 
    this.loader.emit(true);
    // 5 milliseconds timeout added here to resolve the video loading issue in android phones
    setTimeout(()=>{
        this.isCapturing = true;
        this.getAttributes();
        this.canvas.width = (this.video.videoWidth);
        this.canvas.height = (this.video.videoHeight);
        this.imagesArray = [];
        this.phoneAnglesArray = [];
        // minimum resolution 1920 * 1440
        /* if (this.video.videoWidth < 1440 || this.video.videoHeight < 1920) {
            this.resolutionError.show();
        } */
        if (this.video.videoWidth < 640 || this.video.videoHeight < 480) {
            this.errorMsg = ["The camera does not support the resolution required for calculation"];
            
            this.showError.show();
            // alert("resolutionError");
        } else {
            try {
                let { height, width, benchmark } = canvasSize.maxArea({usePromise: true }).catch(err => { console.log(err)}) 
                let canvasHeight =  this.canvas.height;
                let canvasWidth =  this.canvas.width;
                if(height) { canvasHeight =  this.canvas.height > height ? height: this.canvas.height }
                if(width) { canvasWidth =  this.canvas.width > width ? width:  this.canvas.width  }
                this.canvas.getContext('2d').drawImage(this.video, 0, 0, this.video.videoWidth, this.video.videoHeight,
                    0, 0, canvasWidth, canvasHeight);                
                this.phoneAnglesArray.push({
                    x : JSON.parse(JSON.stringify(Math.floor(this.phoneAngle.z))),
                    y : JSON.parse(JSON.stringify(Math.floor(this.phoneAngle.y)))
                });
                console.log(this.canvas.toDataURL('image/png'));
                if (this.canvas.toDataURL('image/png')) {
                    this.imagesArray.push(this.canvas.toDataURL('image/png'));
                } else {
                    this.imagesArray.push(null);
                }
                this.captureImageLoop();
            } catch (err) {
                console.log("Error drawing image")
            }
        }
    }, 5);        
};

/**
 * Function to capture images in every 50 milliseconds interval for 3 times
 */
captureImageLoop() {
    let i = 0;
    const captureInterval = setInterval(() => {
        i++
        let { height, width, benchmark } = canvasSize.maxArea({usePromise: true }).catch(err => { console.log(err)});
        let canvasHeight =  this.canvas.height;
        let canvasWidth =  this.canvas.width;
        if(height) { canvasHeight =  this.canvas.height > height ? height: this.canvas.height }
        if(width) { canvasWidth =  this.canvas.width > width ? width:  this.canvas.width  }
        this.phoneAnglesArray.push({
            x : JSON.parse(JSON.stringify(Math.floor(this.phoneAngle.z))),
            y : JSON.parse(JSON.stringify(Math.floor(this.phoneAngle.y)))
        });

        this.canvas.getContext('2d').drawImage(this.video, 0, 0, this.video.videoWidth, this.video.videoHeight,
        0, 0, canvasWidth, canvasHeight);
        console.log(this.canvas.toDataURL('image/png'));
        if (this.canvas.toDataURL('image/png')) {
            this.imagesArray.push(this.canvas.toDataURL('image/png'));
        } else {
            this.imagesArray.push(null);
        } 
        if (i ==  this.captureCount) {
            i=0;
            clearInterval(captureInterval);
            console.log('this.imagesArray', this.imagesArray.length, this.phoneAnglesArray.length, this.phoneAnglesArray);                
            this.getMeasurements(this.imagesArray, this.phoneAnglesArray);
            this.isCapturing = false;       
        }
    }, 50); 
}

优化方案

1. 缓存Canvas上下文,避免重复获取

每次截图调用getContext('2d')会产生额外性能开销,建议初始化时获取一次并缓存:

ctx: CanvasRenderingContext2D | null = null;

ngAfterViewInit() {
    this.canvas = document.querySelector('canvas');
    this.ctx = this.canvas?.getContext('2d');
}

后续截图直接使用this.ctx调用drawImage方法。

2. 预计算Canvas最大尺寸,避免重复调用

当前每次截图都异步调用canvasSize.maxArea却用同步方式取值,既无法获取正确结果,又重复计算浪费资源。建议初始化时预计算一次并复用:

maxCanvasWidth = 0;
maxCanvasHeight = 0;

async ngAfterViewInit() {
    try {
        const { height, width } = await canvasSize.maxArea({usePromise: true});
        this.maxCanvasWidth = width;
        this.maxCanvasHeight = height;
    } catch(err) {
        console.log(err);
    }
}

截图时直接使用缓存的maxCanvasWidth和maxCanvasHeight。

3. 减少toDataURL重复调用

toDataURL('image/png')是Android端性能瓶颈之一,当前每次截图调用两次,改为只调用一次并缓存结果:

const imageDataUrl = this.canvas.toDataURL('image/png');
console.log(imageDataUrl);
this.imagesArray.push(imageDataUrl || null);

4. 用requestAnimationFrame替代setInterval

setInterval在主线程繁忙时会延迟执行,requestAnimationFrame与浏览器渲染帧同步,能保证截图间隔更稳定:

captureImageLoop() {
    let i = 0;
    const captureFrame = () => {
        // 复用预计算的尺寸
        let canvasHeight = this.canvas.height;
        let canvasWidth = this.canvas.width;
        if(this.maxCanvasHeight) { canvasHeight = this.canvas.height > this.maxCanvasHeight ? this.maxCanvasHeight : this.canvas.height }
        if(this.maxCanvasWidth) { canvasWidth = this.canvas.width > this.maxCanvasWidth ? this.maxCanvasWidth : this.canvas.width  }
        
        this.phoneAnglesArray.push({
            x : Math.floor(this.phoneAngle.z),
            y : Math.floor(this.phoneAngle.y)
        });

        this.ctx?.drawImage(this.video, 0, 0, this.video.videoWidth, this.video.videoHeight,
                    0, 0, canvasWidth, canvasHeight);
        
        const imageDataUrl = this.canvas.toDataURL('image/png');
        console.log(imageDataUrl);
        this.imagesArray.push(imageDataUrl || null); 

        i++;
        if (i < this.captureCount) {
            setTimeout(() => requestAnimationFrame(captureFrame), 50);
        } else {
            console.log('this.imagesArray', this.imagesArray.length, this.phoneAnglesArray.length, this.phoneAnglesArray);                
            this.getMeasurements(this.imagesArray, this.phoneAnglesArray);
            this.isCapturing = false;
        }
    };
    requestAnimationFrame(captureFrame);
}

5. 动态降低Canvas分辨率(业务允许的情况下)

高分辨率Canvas的绘制和转码会大幅增加Android设备的负载,可根据设备性能动态缩小尺寸:

const scale = 0.5; // 可根据设备调整
this.canvas.width = Math.floor(this.video.videoWidth * scale);
this.canvas.height = Math.floor(this.video.videoHeight * scale);

this.ctx?.drawImage(this.video, 0, 0, this.video.videoWidth, this.video.videoHeight,
                    0, 0, this.canvas.width, this.canvas.height);

6. 替代方案:使用WebCodecs API

如果目标Android浏览器支持WebCodecs API(Chrome 94+),可以直接从视频流中提取帧,跳过Canvas的中间步骤,性能会大幅提升。WebCodecs专门用于高效处理音视频帧,适合连续截图场景。

内容的提问来源于stack exchange,提问作者Rajagopal Ganesan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 11:54:57