如何在 Javascript 中选择合适的后置摄像头?

Posted

技术标签:

【中文标题】如何在 Javascript 中选择合适的后置摄像头?【英文标题】:How to select proper backfacing camera in Javascript? 【发布时间】:2020-04-25 10:26:14 【问题描述】:

我正在使用navigator.mediaDevices.getUserMedia 在网络浏览器中从相机设备打开MediaStream。我的应用想要在 WebAssembly 中进行一些实时图像处理,为此,我需要直接从相机提供实时图像流。

我的解决方案在大多数设备上运行良好,但是,我在具有多个后置摄像头的设备上遇到了问题,例如适用于 android 的 Google Chrome 上的三星 Galaxy S10。问题是下面的sn-p:

const constraints = 
    audio: false,
    video: 
        width:  min: 640, ideal: 1280, max: 1920 ,
        height:  min: 480, ideal: 720, max: 1080 ,
        facingMode:  ideal: 'environment' ,
    
;
const stream = await navigator.mediaDevices.getUserMedia( constraints );

总是打开错误的相机 - 广角相机不支持自动对焦,并且提供的图像对于我的代码来说太失真了。广角相机非常适合风景摄影,但对于条形码和文本扫描来说很糟糕。

如何使用MediaTrackConstraints 选择正确的相机?我也试过添加

focusMode:  ideal: 'continuous' 

受约束(根据MDN documentation,这应该是图像轨道的可能约束),但它似乎不起作用。

我也试过枚举所有设备(来自this SO answer),但我不知道如何正确选择正确的相机。

值得注意的是,这段代码sn-p:

const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach( ( device: MediaDeviceInfo ) => 
    console.log( "Found device: " + JSON.stringify( device ) );
);

产生以下控制台输出:

Found device: "deviceId":"default","kind":"audioinput","label":"","groupId":"4852f187ff6a41e6d3fb3ba41c4897f46bd8ff153579da6fcb8f485432a32f66"
Found device: "deviceId":"86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d","kind":"audioinput","label":"","groupId":"c2cfc78763f7668263b0033c44d0f906ca0f33264ebfa6b96e9846265a21ff09"
Found device: "deviceId":"4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c","kind":"audioinput","label":"","groupId":"7a86866423279d7b1e12dbe585a14a677a2f2df4e41ec5d388b6c90f7319e88d"
Found device: "deviceId":"b46cd34041256d2cf72ed6e8500f71beb698a01b8c47c7c04801c20c47630978","kind":"videoinput","label":"camera2 1, facing front","groupId":"b1bd1a6ed8a87cd07ca0fa84744ae515b1ab2bed61cc257765c37d3426269af7"
Found device: "deviceId":"39d63e8a9764261b73785c90beb58399997a5a4de56b3238fff6676c738331a6","kind":"videoinput","label":"camera2 3, facing front","groupId":"a23c2f0e311c0ca0c80a56a8b5ff7c1f8aa093df4f6ac080b051c1d95f60a94e"
Found device: "deviceId":"4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c","kind":"videoinput","label":"camera2 2, facing back","groupId":"9b6b1a429e0db2d5094ddebe205d23309464650d8bcd585b2fe4ae8196b86f1c"
Found device: "deviceId":"86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d","kind":"videoinput","label":"camera2 0, facing back","groupId":"0de9556e1253763d7203b3b9e5db313cf89e05dd4bdd4ea0c5aff52d2952cf11"
Found device: "deviceId":"default","kind":"audiooutput","label":"","groupId":"default"

正确的相机标签为camera2 0, facing back,由于某种原因,Chrome 总是选择camera2 2, facing back

当然,我可以通过标签对摄像头的选择进行硬编码,但这仅适用于三星 Galaxy S10,我希望我的代码适用于任何具有多个后置摄像头的设备。

我还没有尝试在 iPhone 11 Pro(有 3 个后置摄像头)上运行我的页面,但它可以在华为 Mate 30 Pro(4 个后置摄像头)和 Oppo Reno 9 上正常运行。另外,问题似乎与 Android 上的 Google Chrome 有关。当我在 Firefox for Android 上打开我的页面时,浏览器会要求我在关闭相机权限对话框后选择应该使用哪个相机,并且只有在多个相机满足给定约束的情况下。这是相当公平的,因为它可以选择正确的相机来执行扫描。我还没有尝试在 Samsung Internet 和 Opera 浏览器上打开我的页面。

由于 Google Chrome 是 Android 上最受欢迎的网络浏览器,我什至会对 Chrome 特定的解决方案感到满意,但当然,最好的办法是获得在任何地方都有效的答案。

编辑

根据jib 的评论,我也尝试使用focusDistance

focusDistance:  min: 0.05, ideal: 0.12, max: 0.3 

它没有帮助。

我还尝试使用以下 sn-p 记录 getSettingsgetCapabilities 的输出:

const devices = await navigator.mediaDevices.enumerateDevices();
let videoDevices: Array< MediaDeviceInfo > = [];
devices.forEach( ( device: MediaDeviceInfo ) => 
    if ( device.kind == 'videoinput' ) 
        console.log( "Found video device: " + JSON.stringify( device ) );
        videoDevices.push( device );
    
);

console.log( '' );

// open every video device and dump its characteristics
for ( let i in videoDevices ) 
    const device = videoDevices[ i ];
    console.log( "Opening video device " + device.deviceId + " (" + device.label + ")" );
    const stream = await navigator.mediaDevices.getUserMedia(  video:  deviceId:  exact: device.deviceId    );
    stream.getVideoTracks().forEach( track => 
            const capabilities = track.getCapabilities();
            console.log( "Track capabilities: " + JSON.stringify( capabilities ) );
            const settings = track.getSettings();
            console.log( "Track settings: " + JSON.stringify( settings ) );
            console.log( '' );
        
    )

    stream.getTracks().forEach( track => track.stop() );

输出如下:

Found video device: "deviceId":"b46cd34041256d2cf72ed6e8500f71beb698a01b8c47c7c04801c20c47630978","kind":"videoinput","label":"camera2 1, facing front","groupId":"500dd57c6795399100a5ca8bf7f0cc4d7ed8b1bcb0877101d1bef7eb74921868"
Found video device: "deviceId":"39d63e8a9764261b73785c90beb58399997a5a4de56b3238fff6676c738331a6","kind":"videoinput","label":"camera2 3, facing front","groupId":"245693c8d34be77fe2f15be31b6054a19edb8ea9ed4116d966d2a03695bebebe"
Found video device: "deviceId":"4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c","kind":"videoinput","label":"camera2 2, facing back","groupId":"c219595df2c2a430aea7007f64e6ce8fbfa783b038cf53069336361cc07e71af"
Found video device: "deviceId":"86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d","kind":"videoinput","label":"camera2 0, facing back","groupId":"96a68b6d6429786317e3b2c4773082604c5ab9a5cdaaf49c481878e40d67e987"

Opening video device b46cd34041256d2cf72ed6e8500f71beb698a01b8c47c7c04801c20c47630978 (camera2 1, facing front)
Track capabilities: "aspectRatio":"max":3216,"min":0.0004528985507246377,"deviceId":"b46cd34041256d2cf72ed6e8500f71beb698a01b8c47c7c04801c20c47630978","facingMode":["user"],"frameRate":"max":30,"min":0,"groupId":"500dd57c6795399100a5ca8bf7f0cc4d7ed8b1bcb0877101d1bef7eb74921868","height":"max":2208,"min":1,"resizeMode":["none","crop-and-scale"],"width":"max":3216,"min":1
Track settings: "aspectRatio":1.3333333333333333,"deviceId":"b46cd34041256d2cf72ed6e8500f71beb698a01b8c47c7c04801c20c47630978","facingMode":"user","frameRate":30,"groupId":"500dd57c6795399100a5ca8bf7f0cc4d7ed8b1bcb0877101d1bef7eb74921868","height":480,"resizeMode":"none","width":640

Opening video device 39d63e8a9764261b73785c90beb58399997a5a4de56b3238fff6676c738331a6 (camera2 3, facing front)
Track capabilities: "aspectRatio":"max":3968,"min":0.0003654970760233918,"deviceId":"39d63e8a9764261b73785c90beb58399997a5a4de56b3238fff6676c738331a6","facingMode":["user"],"frameRate":"max":30,"min":0,"groupId":"245693c8d34be77fe2f15be31b6054a19edb8ea9ed4116d966d2a03695bebebe","height":"max":2736,"min":1,"resizeMode":["none","crop-and-scale"],"width":"max":3968,"min":1
Track settings: "aspectRatio":1.3333333333333333,"deviceId":"39d63e8a9764261b73785c90beb58399997a5a4de56b3238fff6676c738331a6","facingMode":"user","frameRate":30,"groupId":"245693c8d34be77fe2f15be31b6054a19edb8ea9ed4116d966d2a03695bebebe","height":480,"resizeMode":"none","width":640

Opening video device 4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c (camera2 2, facing back)
Track capabilities: "aspectRatio":"max":4608,"min":0.00028935185185185184,"deviceId":"4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c","facingMode":["environment"],"frameRate":"max":60,"min":0,"groupId":"c219595df2c2a430aea7007f64e6ce8fbfa783b038cf53069336361cc07e71af","height":"max":3456,"min":1,"resizeMode":["none","crop-and-scale"],"width":"max":4608,"min":1
Track settings: "aspectRatio":1.3333333333333333,"deviceId":"4d5fecf5a3eee5d41812bb6c34efe6d25342af9448628b006561c7385a22ca6c","facingMode":"environment","frameRate":60,"groupId":"c219595df2c2a430aea7007f64e6ce8fbfa783b038cf53069336361cc07e71af","height":480,"resizeMode":"none","width":640

Opening video device 86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d (camera2 0, facing back)
Track capabilities: "aspectRatio":"max":4032,"min":0.00033068783068783067,"deviceId":"86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d","facingMode":["environment"],"frameRate":"max":60,"min":0,"groupId":"96a68b6d6429786317e3b2c4773082604c5ab9a5cdaaf49c481878e40d67e987","height":"max":3024,"min":1,"resizeMode":["none","crop-and-scale"],"width":"max":4032,"min":1
Track settings: "aspectRatio":1.3333333333333333,"deviceId":"86d4706b0bf160ff12fa75535173edcc68d4fa7ad5e00ec186cb1285ff22869d","facingMode":"environment","frameRate":60,"groupId":"96a68b6d6429786317e3b2c4773082604c5ab9a5cdaaf49c481878e40d67e987","height":480,"resizeMode":"none","width":640

所以,除了最大可用分辨率不同之外,camera2 0, facing backcamera2 2, facing back 之间没有区别。

我也尝试过使用三星互联网浏览器,它的行为与 Google Chrome 相同。

还有其他想法吗(除了遍历所有后置摄像头并选择分辨率最低的摄像头)?

【问题讨论】:

好问题。也许尝试从两个摄像头查看deviceInfo.getCapabilities()track.getSettings() 并寻找差异。你试过focusDistance吗? 刚刚找到this little gem。这很 hacky,但它可以工作。 我已经为此提交了issue with the spec。 @Frogger, have fun. 但是facingModeconstraint 对象的属性,而生成的device 对象没有它。检查上面给出的控制台输出。 【参考方案1】:

我将所有相机按 id 放入这样的数组中

      navigator.mediaDevices.enumerateDevices()
        .then(function(devices) 
        
        for(;devices[i];)
        if(devices[i].kind == "videoinput")
            that.aCameras.push(   [devices[i].deviceId , devices[i].label]   )
            j++;            
            
        i++;
        
    );

在按下按钮翻转相机的事件中,我这样做了:

      var defaultsOpts =  audio: false, video: true ;
      defaultsOpts.video =  
              deviceId: that.aCameras[that.currentCamera][0]
      ;
      if ( that.aCameras.length-1 != that.currentCamera )
          that.currentCamera++;
      
      else
          that.currentCamera = 0;
            
      navigator.mediaDevices.getUserMedia(defaultsOpts)
            .then(function (stream) 
                  vid.srcObject = stream;
                  localstream = stream;
                  vid.play();
               );
          
       );

这样,而不是使用用户/环境, 我的问题已经解决了。

希望对你也有帮助。

问候, AVI。

【讨论】:

这看起来不正确。它假设阵列中的第 n+1 个摄像头与第 n 个摄像头的方向相反,这是不正确的。查看问题 - 三星 S10 连续列出了前后摄像头,无法区分它们。就我而言,选择哪一个是相关的。【参考方案2】:

由于这个问题在今天仍然存在,目前检测“非长焦”/“非广角”相机的最佳方法是恕我直言,只需检查 torch 参数。

(因为这是字面上唯一的参数,在某些设备上与其他“标准”相机不同。)

我正在这样做:

打开默认摄像头流(=facingMode: ideal: 'environment' ,,找出torch是否存在 如果没有,请关闭此摄像头流并针对每个设备进行迭代;尝试检测torch 如果没有找到,回退到第一个摄像头;或者可能通过其他参数的某种组合变得更好 - 例如。 focusDistance。 (将选定的相机 ID 保存到例如 cookie 中,以便该用户下次更快)

【讨论】:

以上是关于如何在 Javascript 中选择合适的后置摄像头?的主要内容,如果未能解决你的问题,请参考以下文章

javascript NavigatorUserMedia.getUserMedia 智能手机后置摄像头

如何在quickblox中默认打开后置摄像头

如何限制插件在android / cordova中切换后置/后置摄像头

uwp之拍照(使用后置摄像头)

双机位考试手机如何使用虚拟摄像头

在 Android 设备上选择后置摄像头 - jsartoolkit5