Chromium/js enumerateDevices device_id & group_id生成机制探索

1,528 阅读12分钟

前言

为什么要分析enumationDevice源码? 由于通过js调用enumationDevice可以获取到设备device_id和gourp_id,但是这个device_id和group_id都是进行过处理的(hash),所以无法得到原始数据,也不知道原始数据是什么,目前需要能够通过源码弄够弄清楚整个device_id和group_id的生成过程,然后利用tinycv返回的数据生成和它一样的hash值,从而实现对应

思路

  1. 硬看chromium源码,找出生成规则
  2. 编译chormium,源码调式

推荐一个在线浏览chromium源码的网站,本地电脑实在太卡了 source.chromium.org/chromium/ch…

tinycv-node

如果你对原理不感兴趣,我已经封装好了一个node库,从这个node库你就可以直接生成和chromium对应的device_id,链接 tinycv, 通过DeviceManager可以枚举出所有Device,在通过Device类可以将device path转换成chromium对应的device id

blink - MediaDevices::enumerateDevices

文件:chromium/third_party/blink/renderer/modules/mediastream/media_devices.cc:96

ScriptPromise MediaDevices::enumerateDevices(ScriptState* script_state,
                                             ExceptionState& exception_state) {
  UpdateWebRTCMethodCount(RTCAPIName::kEnumerateDevices);
  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Current frame is detached.");
    return ScriptPromise();
  }

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
  ScriptPromise promise = resolver->Promise();
  requests_.insert(resolver);

  LocalFrame* frame = LocalDOMWindow::From(script_state)->GetFrame();
  GetDispatcherHost(frame)->EnumerateDevices(
      true /* audio input */, true /* video input */, true /* audio output */,
      true /* request_video_input_capabilities */,
      true /* request_audio_input_capabilities */,
      WTF::Bind(&MediaDevices::DevicesEnumerated, WrapPersistent(this),
                WrapPersistent(resolver)));
  return promise;
}

上面的函数就是js调用enumerateDevices时具体的实现,首先我们要清楚chromium的多进程框架,blink里面的代码都是在render进程里面运行,render进程只会负责dom结构解析以及js解析执行等,通过上面代码我们看到核心代码 GetDispatcherHost(frame)->EnumerateDevices(),枚举设备这个涉及到操作系统,真正的实现肯定不会在blink里面,也就是不会在render进程里面,而是在browser进程里面,而chromium自己又有一套它的多进程通信框架mojo,这里GetDispatcherHost就是通过mojo将EnumerateDevices真实操作交由browser进程处理,可以接着看下面GetDispatcerHost实现


blink - MediaDevices::GetDispatcherHost

chromium/third_party/blink/renderer/modules/mediastream/media_devices.cc:502

const mojo::Remote<mojom::blink::MediaDevicesDispatcherHost>&
MediaDevices::GetDispatcherHost(LocalFrame* frame) {
  if (!dispatcher_host_) {
    // ....
  }

  return dispatcher_host_;
}

从上面可以看到这就是一个mojom对象,mojom是chromium里面为了mojo进程间通信开发的一套接口描述语言(IDL),类似于protobuf,通过接口描述语言可以生成对应的c++文件,关于mojo更多信息,chromium官网有详细介绍, 可以从返回值看出对象

mojom::blink::MediaDevicesDispatcherHost

chromium/third_party/blink/public/mojom/mediastream/media_devices.mojom

// This object lives in the browser and is responsible for processing device
// enumeration requests and managing subscriptions for device-change
// notifications.
interface MediaDevicesDispatcherHost {
  EnumerateDevices(bool request_audio_input,
                   bool request_video_input,
                   bool request_audio_output,
                   bool request_video_input_capabilities,
                   bool request_audio_input_capabilities)
      => (array<array<MediaDeviceInfo>> enumeration,
          array<VideoInputDeviceCapabilities> video_input_device_capabilities,
          array<AudioInputDeviceCapabilities> audio_input_device_capabilities);
}

上面的代码就是mojom定义的MediaDevicesDispatcherHost接口对象,它在编译的时候会生成一个对应的.h文件,C++这边只需要继承这个MediaDevicesDispatherHost,并实现对应的接口即可, 那下面的思路就是全局搜索谁继承了这个类

browser - content::MediaDeviceDispatcherHost

chromium/content/browser/renderer_host/media/media_devices_dispatcher_host.h

class CONTENT_EXPORT MediaDevicesDispatcherHost
    : public blink::mojom::MediaDevicesDispatcherHost {
 public:
  // ......

  // blink::mojom::MediaDevicesDispatcherHost implementation.
  void EnumerateDevices(bool request_audio_input,
                        bool request_video_input,
                        bool request_audio_output,
                        bool request_video_input_capabilities,
                        bool request_audio_input_capabilities,
                        EnumerateDevicesCallback client_callback) override;
  // ......

可以看到在content里面实现了blink::mojom::MediaDevicesDispatcherHost, 从代码注释也可以清楚的看出来他就是mojom接口的实现,到这里我们也就走上了正轨.

MediaDeviceDispatcherHost::EnumerateDevices

chromium/content/browser/renderer_host/media/media_devices_dispatcher_host.cc:123

void MediaDevicesDispatcherHost::EnumerateDevices(
    bool request_audio_input,
    bool request_video_input,
    bool request_audio_output,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback client_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  // ......
  MediaDevicesManager::BoolDeviceTypes devices_to_enumerate;
  devices_to_enumerate[static_cast<size_t>(
      MediaDeviceType::MEDIA_AUDIO_INPUT)] = request_audio_input;
  devices_to_enumerate[static_cast<size_t>(
      MediaDeviceType::MEDIA_VIDEO_INPUT)] = request_video_input;
  devices_to_enumerate[static_cast<size_t>(
      MediaDeviceType::MEDIA_AUDIO_OUTPUT)] = request_audio_output;

  media_stream_manager_->media_devices_manager()->EnumerateDevices(
      render_process_id_, render_frame_id_, devices_to_enumerate,
      request_video_input_capabilities, request_audio_input_capabilities,
      std::move(client_callback));
}

上面函数会调用到media_devices_manager函数到MediaDeviceManager::EnumerateDevices

MediaDeviceManger::EnumerateDevices

chromium/content/browser/renderer_host/media/media_devices_manager.cc

void MediaDevicesManager::EnumerateDevices(
    int render_process_id,
    int render_frame_id,
    const BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  base::PostTaskAndReplyWithResult(
      GetUIThreadTaskRunner({}).get(), FROM_HERE,
      base::BindOnce(salt_and_origin_callback_, render_process_id,
                     render_frame_id),
      base::BindOnce(&MediaDevicesManager::CheckPermissionsForEnumerateDevices,
                     weak_factory_.GetWeakPtr(), render_process_id,
                     render_frame_id, requested_types,
                     request_video_input_capabilities,
                     request_audio_input_capabilities, std::move(callback)));
}

上面代码使用到了一个chromium的一个特性多线程通信,PostTaskAndReplayWithResult函数会将salt_and_origin_callback_这个函数Post到UI线程执行,然后执行之后的结果会回掉到CheckPermissionsForEnumerateDevices, 这个CheckPermissionsForEnumerateDevices会在IO线程执行, 关于salt_and_origin_callback_这个成员变量,它其实就是一个function地址,对应的function是GetMediaDeviceSaltAndOrigin, 通过这个函数名称其实不难猜出来,我们要找的device_id和group_id肯定是加盐之后在hash,通过这个函数我们就可以找到加的什么盐

GetMediaDeviceSaltAndOrigin

chromium/content/content/browser/media/media_devices_util.cc

MediaDeviceSaltAndOrigin GetMediaDeviceSaltAndOrigin(int render_process_id,
                                                     int render_frame_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  RenderFrameHostImpl* frame_host =
      RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
  RenderProcessHost* process_host =
      RenderProcessHost::FromID(render_process_id);

  // ......

  if (frame_host) {
    // ......
    origin = frame_host->GetLastCommittedOrigin();
    frame_salt = frame_host->GetMediaDeviceIDSaltBase();
  }

  bool are_persistent_ids_allowed = false;
  std::string device_id_salt;
  std::string group_id_salt;
  if (process_host) {
    are_persistent_ids_allowed =
        GetContentClient()->browser()->ArePersistentMediaDeviceIDsAllowed(
            process_host->GetBrowserContext(), url, site_for_cookies,
            top_level_origin);
    device_id_salt = process_host->GetBrowserContext()->GetMediaDeviceIDSalt();
    group_id_salt = device_id_salt;
  }

  // If persistent IDs are not allowed, append |frame_salt| to make it
  // specific to the current document.
  if (!are_persistent_ids_allowed)
    device_id_salt += frame_salt;

  // |group_id_salt| must be unique per document, but it must also change if
  // cookies are cleared. Also, it must be different from |device_id_salt|,
  // thus appending a constant.
  group_id_salt += frame_salt + "groupid";

  return {std::move(device_id_salt), std::move(group_id_salt),
          std::move(origin)};
}

首先我目前知道的是即使是关闭浏览器重新启动device_id也不会变更,只有在重新插拔设备才会变更,这就确定了一点上面的frame_salt是不会被用上(从注释看), 所以我们目前只需要分析 device_id_salt这个值是怎么生成的, 那就只需要关注一行代码就是(暂时忽略group_id,从注释上看group_id就变化的, 因为frame_salt是变化的),备注:其实device_id不会变更分析到后面的时候否定了这个结论 process_host->GetBrowserContext()->GetMediaDeviceIDSalt()

ProfileImpl::GetMediaDeviceIDSalt

chromium/chrome/browser/profiles/profile_impl.cc:1341

std::string ProfileImpl::GetMediaDeviceIDSalt() {
  return media_device_id_salt_->GetSalt();
}

MediaDeviceIDSalt::GetSalt

chromium/chrome/browser/media/media_device_id_salt.cc

std::string MediaDeviceIDSalt::GetSalt() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return media_device_id_salt_.GetValue();
}

GetSalt函数返回了media_device_id_salt里面的value,目前我们只需要找到media_device_id_salt对象的初始化和SetValue即可

MediaDeviceIDSalt::MediaDeviceIDSalt

chromium/chrome/browser/media/media_device_id_salt.cc

MediaDeviceIDSalt::MediaDeviceIDSalt(PrefService* pref_service) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  media_device_id_salt_.Init(prefs::kMediaDeviceIdSalt, pref_service);
  if (media_device_id_salt_.GetValue().empty()) {
    media_device_id_salt_.SetValue(
        content::BrowserContext::CreateRandomMediaDeviceIDSalt());
  }
}

在MediaDeviceIDSalt构造函数里面找到了初始化, 不过有很大疑问,这里的SetValue设置的是一个随机值,CreateRandomMediaDeviceIDSalt函数跳进去看实现确实就是生成的一个随机值,但是呢device_id在重启浏览器之后都不会变更,那这个盐就不可能是一个随机值,二种情况

  1. 整个分析错误了,GetSalt并不是走到这里,可能走到了别的地方?(毕竟是硬看源码,没有调式,所以有可能)
  2. media_device_id_salt.GetValue().empty() 函数没有满足,从而没有走到SetValue一个随机值

device_id & group_id salt生成结论

源码调式之后的结论:在程序启动之后,调用enumerateDevices之后发现他会直接调用到 MediaDeviceIDSalt::GetSalt(),这个里面到GetValue值早已经存在,然后断点下载MediaDeviceIDSalt构造函数里面,再次调式,发现media_device_id_salt_.GetValue().empty()这个条件并没有进去,也就是说明这个值已经存在了,这就很疑惑了,为啥直接就存在了,然后仔细看了很长时间也只发现在media_device_id_salt_.SetValue这里会复制这个salt,就很疑惑,之后接着深入去看chromium到profile,发现它会将所有配置保存到文件,那这样media_device_id_salt.GetValue().empty()不为空就可能了

  1. 在第一次调式到时候没有注意整个浏览器到初始化,而是直接通过浏览器直接调用enumerateDevices,导致整个profile已经初始化且写入到了文件,在调式已经没有多大意义
  2. 后续到重新调式需要已经意识到这个值很可能是在浏览器启动就已经生成了,但是由于文件缓存已经有了,所以media_device_id_salt_.SetValue这一行代码不会执行,导致耗费了非常多到时间去找media_device_id_salt值初始化(主要由于猜想由pref_service传入到构造函数就已经完成了生成,不过确实如此,在pref_service中其实已经保存了文件缓存到值,只是这些代码不好找到)
  3. 发现了这一点,开始尝试删除chromium所有用户文件,再次进行调式,发现进入了media_device_id_salt_.SetValue,然后生成了一个随机值,并且缓存到了“AppData\Local\Chromium\User Data\Default\Preferences" 文件(electron在自己的安装目录下的Preferences文件,这个文件是一个json格式,到这里寻找device_id和group_id salt也就已经结束了

device_id和group_id salt是一个随机数,会在第一次chromium初始化的时候生成并且写入到Preferences文件,用户可以通过读取Preferences文件获取到media_devcie_id_salt的值(json格式)

下面将接着上面的enumerateDevices流程走完,继续寻找device_id和group_id原始数据,以及它们的hash方式

MediaDevicesManager::CheckPermissionsForEnumerateDevices

chromium/content/browser/renderer_host/media/media_devices_manager.cc:663

void MediaDevicesManager::CheckPermissionsForEnumerateDevices(
   int render_process_id,
   int render_frame_id,
   const BoolDeviceTypes& requested_types,
   bool request_video_input_capabilities,
   bool request_audio_input_capabilities,
   EnumerateDevicesCallback callback,
   MediaDeviceSaltAndOrigin salt_and_origin) {
 DCHECK_CURRENTLY_ON(BrowserThread::IO);
 permission_checker_->CheckPermissions(
     requested_types, render_process_id, render_frame_id,
     base::BindOnce(&MediaDevicesManager::OnPermissionsCheckDone,
                    weak_factory_.GetWeakPtr(), requested_types,
                    request_video_input_capabilities,
                    request_audio_input_capabilities, std::move(callback),
                    std::move(salt_and_origin)));
}

接着上面的EnumerateDevices分析,当他调用完GetMediaDeviceSaltAndOrigin会将结果返回到MediaDevicesManager::CheckPermissionsForEnumerateDevices, 这个函数里面又调用了CheckPermissions函数并且会讲结果返回到OnPermissionsCheckDone函数

MediaDevicesManager::OnPermissionsCheckDone

chromium/content/browser/renderer_host/media/media_devices_manager.cc:663

void MediaDevicesManager::OnPermissionsCheckDone(
   const MediaDevicesManager::BoolDeviceTypes& requested_types,
   bool request_video_input_capabilities,
   bool request_audio_input_capabilities,
   EnumerateDevicesCallback callback,
   MediaDeviceSaltAndOrigin salt_and_origin,
   const MediaDevicesManager::BoolDeviceTypes& has_permissions) {
 DCHECK_CURRENTLY_ON(BrowserThread::IO);
 // ......

 EnumerateDevices(
     internal_requested_types,
     base::BindOnce(&MediaDevicesManager::OnDevicesEnumerated,
                    weak_factory_.GetWeakPtr(), requested_types,
                    request_video_input_capabilities,
                    request_audio_input_capabilities, std::move(callback),
                    std::move(salt_and_origin), has_permissions));
}

调用 EnumerateDevices结果回调到OnDevicesEnumerated

MediaDevicesManager::EnumerateDevices

chromium/content/browser/renderer_host/media/media_devices_manager.cc:405

void MediaDevicesManager::EnumerateDevices(
   const BoolDeviceTypes& requested_types,
   EnumerationCallback callback) {
 DCHECK_CURRENTLY_ON(BrowserThread::IO);
 StartMonitoring();

 requests_.emplace_back(requested_types, std::move(callback));
 bool all_results_cached = true;
 for (size_t i = 0;
      i < static_cast<size_t>(MediaDeviceType::NUM_MEDIA_DEVICE_TYPES); ++i) {
   if (requested_types[i] && cache_policies_[i] == CachePolicy::NO_CACHE) {
     all_results_cached = false;
     DoEnumerateDevices(static_cast<MediaDeviceType>(i));
   }
 }

 if (all_results_cached)
   ProcessRequests();
}

忽略一些不关心代码,也就是这个函数调用了DoEnumerateDevies

MediaDevicesManager::DoEnumerateDevices

chromium/content/browser/renderer_host/media/media_devices_manager.cc:877

void MediaDevicesManager::DoEnumerateDevices(MediaDeviceType type) {
  // ......

 switch (type) {
   case MediaDeviceType::MEDIA_AUDIO_INPUT:
     EnumerateAudioDevices(true /* is_input */);
     break;
   case MediaDeviceType::MEDIA_VIDEO_INPUT:
     video_capture_manager_->EnumerateDevices(
         base::BindOnce(&MediaDevicesManager::VideoInputDevicesEnumerated,
                        weak_factory_.GetWeakPtr()));
     break;
   case MediaDeviceType::MEDIA_AUDIO_OUTPUT:
     EnumerateAudioDevices(false /* is_input */);
     break;
   default:
     NOTREACHED();
 }
}

忽略掉不关心代码,这个switch就是调用到了video_capture_manager_->EnumerateDevices,然后结果回调到了MediaDevicesManager::VideoInputDevicesEnumerated

VideoCaptureManager

chromium/content/browser/renderer_host/media/video_capture_manager.cc

void VideoCaptureManager::EnumerateDevices(
    EnumerationCallback client_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  EmitLogMessage("VideoCaptureManager::EnumerateDevices", 1);

  // Pass a timer for UMA histogram collection.
  video_capture_provider_->GetDeviceInfosAsync(media::BindToCurrentLoop(
      base::BindOnce(&VideoCaptureManager::OnDeviceInfosReceived, this,
                     base::ElapsedTimer(), std::move(client_callback))));
}

void VideoCaptureManager::OnDeviceInfosReceived(
    base::ElapsedTimer timer,
    EnumerationCallback client_callback,
    const std::vector<media::VideoCaptureDeviceInfo>& device_infos) {
  // ......

  // Walk the |devices_info_cache_| and produce a
  // media::VideoCaptureDeviceDescriptors for |client_callback|.
  media::VideoCaptureDeviceDescriptors devices;
  std::vector<std::tuple<media::VideoCaptureDeviceDescriptor,
                         media::VideoCaptureFormats>>
      descriptors_and_formats;
  for (const auto& it : devices_info_cache_) {
    devices.emplace_back(it.descriptor);
    descriptors_and_formats.emplace_back(it.descriptor, it.supported_formats);
    MediaInternals::GetInstance()->UpdateVideoCaptureDeviceCapabilities(
        descriptors_and_formats);
  }

  std::move(client_callback).Run(devices);
}

EnumerateDevices这个函数里面调用了GetDeviceInfosAsync并且结果会回调到OnDeviceInfoReceicved,OnDeviceInfoReceicved函数干的最主要的一件事就是将冲GetDeviceInfosAsync里面获取到到VideoCaptureInfo转换成VideoCaptureDeviceDescriptors

struct CAPTURE_EXPORT VideoCaptureDeviceInfo {
  VideoCaptureDeviceInfo();
  VideoCaptureDeviceInfo(VideoCaptureDeviceDescriptor descriptor);
  VideoCaptureDeviceInfo(const VideoCaptureDeviceInfo& other);
  ~VideoCaptureDeviceInfo();
  VideoCaptureDeviceInfo& operator=(const VideoCaptureDeviceInfo& other);

  VideoCaptureDeviceDescriptor descriptor;
  VideoCaptureFormats supported_formats;
};

struct CAPTURE_EXPORT VideoCaptureDeviceDescriptor {
 public:
  VideoCaptureDeviceDescriptor();
  VideoCaptureDeviceDescriptor(
      const std::string& display_name,
      const std::string& device_id,
      VideoCaptureApi capture_api = VideoCaptureApi::UNKNOWN,
      const VideoCaptureControlSupport& control_support =
          VideoCaptureControlSupport(),
      VideoCaptureTransportType transport_type =
          VideoCaptureTransportType::OTHER_TRANSPORT);
  VideoCaptureDeviceDescriptor(
      const std::string& display_name,
      const std::string& device_id,
      const std::string& model_id,
      VideoCaptureApi capture_api,
      const VideoCaptureControlSupport& control_support,
      VideoCaptureTransportType transport_type =
          VideoCaptureTransportType::OTHER_TRANSPORT,
      VideoFacingMode facing = VideoFacingMode::MEDIA_VIDEO_FACING_NONE);
 // ......

  std::string device_id;
  // A unique hardware identifier of the capture device.
  // It is of the form "[vid]:[pid]" when a USB device is detected, and empty
  // otherwise.
  std::string model_id;

  VideoFacingMode facing;

  VideoCaptureApi capture_api;
  VideoCaptureTransportType transport_type;

 private:
  std::string display_name_;  // Name that is intended for display in the UI
  VideoCaptureControlSupport control_support_;
};

其中VideoCaptureDeviceInfo就包含了VideoCaptureDeviceDescriptor,而VideoCaptureDeviceDescriptor结构体就是我们想要的数据,它里面有device_id, model_id, display_name等

device_id原始数据结论

经过源码调式,发现device_id在windows上就是device_path

接着看上面OnDeviceInfosReceived的最后一行代码std::move(client_callback).Run(devices); ,它会调用到MediaDevicesManager::VideoInputDevicesEnumerated函数

MediaDevicesManager::VideoInputDevicesEnumerated

chromium/content/browser/renderer_host/media/media_devices_manager.cc:921

void MediaDevicesManager::VideoInputDevicesEnumerated(
    const media::VideoCaptureDeviceDescriptors& descriptors) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  blink::WebMediaDeviceInfoArray snapshot;
  for (const auto& descriptor : descriptors) {
    snapshot.emplace_back(descriptor);
  }
  DevicesEnumerated(MediaDeviceType::MEDIA_VIDEO_INPUT, snapshot);
}

这个函数也非常简单就是将VideoCaptureDeviceDescriptors 转换成WebMediaDeviceInfoArray, 然后在调用了DevicesEnumerated

WebMediaDeviceInfoArray & WebMediaDeviceInfo

chromium/third_party/blink/public/common/mediastream/media_devices.h

using WebMediaDeviceInfoArray = std::vector<WebMediaDeviceInfo>;

struct BLINK_COMMON_EXPORT WebMediaDeviceInfo {
  WebMediaDeviceInfo();
  WebMediaDeviceInfo(const WebMediaDeviceInfo& other);
  WebMediaDeviceInfo(WebMediaDeviceInfo&& other);
  WebMediaDeviceInfo(
      const std::string& device_id,
      const std::string& label,
      const std::string& group_id,
      const media::VideoCaptureControlSupport& video_control_support =
          media::VideoCaptureControlSupport(),
      blink::mojom::FacingMode video_facing = blink::mojom::FacingMode::NONE);
  explicit WebMediaDeviceInfo(
      const media::VideoCaptureDeviceDescriptor& descriptor);

  std::string device_id;
  std::string label;
  std::string group_id;
  media::VideoCaptureControlSupport video_control_support;
  blink::mojom::FacingMode video_facing = blink::mojom::FacingMode::NONE;
};

好,我们已经找到了devie_id和group_id了,这个结构体已经和js返回到数据完美对应了,device_id到原始数据我们已经知道了,就是device_path,现在差group_id,上面的VideoInputDevicesEnumerated函数会调用到explicit WebMediaDeviceInfo( const media::VideoCaptureDeviceDescriptor& descriptor);这个构造函数

WebMediaDeviceInfo::WebMediaDeviceInfo

chromium/third_party/blink/common/mediastream/media_devices.cc:29

WebMediaDeviceInfo::WebMediaDeviceInfo(
    const media::VideoCaptureDeviceDescriptor& descriptor)
    : device_id(descriptor.device_id),
      label(descriptor.GetNameAndModel()),
      video_control_support(descriptor.control_support()),
      video_facing(static_cast<blink::mojom::FacingMode>(descriptor.facing)) {}

可以看到在这个构造函数执行完group_id还是空

MediaDevicesManager::ProcessRequests

chromium/content/browser/renderer_host/media/media_devices_manager.cc:1019

void MediaDevicesManager::ProcessRequests() {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  // Populate the group ID field for video devices using a heuristic that looks
  // for device coincidences with audio input devices.
  // TODO(crbug.com/627793): Remove this once the video-capture subsystem
  // supports group IDs.
  if (has_seen_result_[static_cast<size_t>(
          MediaDeviceType::MEDIA_VIDEO_INPUT)]) {
    blink::WebMediaDeviceInfoArray video_devices =
        current_snapshot_[static_cast<size_t>(
            MediaDeviceType::MEDIA_VIDEO_INPUT)];
    for (auto& video_device_info : video_devices) {
      video_device_info.group_id =
          GuessVideoGroupID(current_snapshot_[static_cast<size_t>(
                                MediaDeviceType::MEDIA_AUDIO_INPUT)],
                            video_device_info);
    }
    UpdateSnapshot(MediaDeviceType::MEDIA_VIDEO_INPUT, video_devices,
                   false /* ignore_group_id */);
  }

  base::EraseIf(requests_, [this](EnumerationRequest& request) {
    if (IsEnumerationRequestReady(request)) {
      std::move(request.callback).Run(current_snapshot_);
      return true;
    }
    return false;
  });
}

从注释中就可以看出来,这就是一个填充gourp ID的函数,具体细节在GuessVideoGroupID函数, 关于GuessVideoGroupID函数就不仔细分析了,它里面就是去匹配和这个video相关联的audio设备,然后拷贝audio设备的groupid(audio设备的groupid是如何生成的就不细分析了)

MediaDevicesManager::OnDevicesEnumerated

chromium/content/browser/renderer_host/media/media_devices_manager.cc:699

void MediaDevicesManager::OnDevicesEnumerated(
    const MediaDevicesManager::BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    const MediaDevicesManager::BoolDeviceTypes& has_permissions,
    const MediaDeviceEnumeration& enumeration) {
  // ......

  std::vector<blink::WebMediaDeviceInfoArray> translation(
      static_cast<size_t>(MediaDeviceType::NUM_MEDIA_DEVICE_TYPES));
  for (size_t i = 0;
       i < static_cast<size_t>(MediaDeviceType::NUM_MEDIA_DEVICE_TYPES); ++i) {
    if (!requested_types[i])
      continue;

    for (const auto& device_info : enumeration[i]) {
      if (base::FeatureList::IsEnabled(
              features::kEnumerateDevicesHideDeviceIDs) &&
          !has_permissions[i] && !translation[i].empty())
        break;

      translation[i].push_back(TranslateMediaDeviceInfo(
          has_permissions[i], salt_and_origin, device_info));
    }
  }
  // ......
}

OnDevicesEnumerated 函数会在这里MediaDevicesManager::OnPermissionsCheckDone里面的EnumerateDevices完成之后触发,这个函数里面有我们要找的hash,就在TranslateMediaDeviceInfo

WebMediaDeviceInfo TranslateMediaDeviceInfo

chromium/content/browser/media/media_devices_util.cc:181

blink::WebMediaDeviceInfo TranslateMediaDeviceInfo(
    bool has_permission,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    const blink::WebMediaDeviceInfo& device_info) {
  return blink::WebMediaDeviceInfo(
      !base::FeatureList::IsEnabled(features::kEnumerateDevicesHideDeviceIDs) ||
              has_permission
          ? GetHMACForMediaDeviceID(salt_and_origin.device_id_salt,
                                    salt_and_origin.origin,
                                    device_info.device_id)
          : std::string(),
      has_permission ? device_info.label : std::string(),
      device_info.group_id.empty()
          ? std::string()
          : GetHMACForMediaDeviceID(salt_and_origin.group_id_salt,
                                    salt_and_origin.origin,
                                    device_info.group_id),
      has_permission ? device_info.video_control_support
                     : media::VideoCaptureControlSupport(),
      has_permission ? device_info.video_facing
                     : blink::mojom::FacingMode::NONE);
}

可以看到这里已经回到了blink里面了,TranslateMediaDeviceInfo里面我们关心的也就是一个函数GetHMACForMediaDeviceID他会使用HMAC生成一个hash值

MediaStreamManager::GetHMACForMediaDeviceID

chromium/content/browser/renderer_host/media/media_stream_manager.cc:2561

std::string MediaStreamManager::GetHMACForMediaDeviceID(
    const std::string& salt,
    const url::Origin& security_origin,
    const std::string& raw_unique_id) {
  // TODO(crbug.com/1215532): DCHECKs are disabled during automated testing on
  // CrOS and this check failed when tested on an experimental builder. Revert
  // https://crrev.com/c/2932244 to enable it. See go/chrome-dcheck-on-cros
  // or http://crbug.com/1113456 for more details.
#if !defined(OS_CHROMEOS)
  DCHECK(!raw_unique_id.empty());
#endif
  if (raw_unique_id == media::AudioDeviceDescription::kDefaultDeviceId ||
      raw_unique_id == media::AudioDeviceDescription::kCommunicationsDeviceId) {
    return raw_unique_id;
  }

  crypto::HMAC hmac(crypto::HMAC::SHA256);
  const size_t digest_length = hmac.DigestLength();
  std::vector<uint8_t> digest(digest_length);
  bool result = hmac.Init(security_origin.Serialize()) &&
                hmac.Sign(raw_unique_id + salt, &digest[0], digest.size());
  DCHECK(result);
  return base::ToLowerASCII(base::HexEncode(&digest[0], digest.size()));
}

到这里已经可以看出使用的是device_id进行加盐然后HMAC sha256,但是还有一个问题就是它使用的是HMAC,并且有一个key,那么这个key也是必须得分析出来,不然我们也生成不出来和它一样的device id,这也就是说在salt不变的情况下,device_id也可能根据这个key的不同发生变化,这个security_origin是通过从上面我们分析的GetMediaDeviceSaltAndOrigin里面获取的, 获取到的是当前浏览器最后一次commit的frame origin

Origin::Serialize

chromium/url/origin.cc

std::string Origin::Serialize() const {
  if (opaque())
    return "null";

  if (scheme() == kFileScheme)
    return "file://";

  return tuple_.Serialize();
}
  1. 如果frame的origin是一个opaque,这个opaque目前我也没有太过于理解,不太清楚什么情况会出现,看代码是某些网站的origin是不公开的,所以就返回空
  2. 当sheme是file时,直接返回"file://"
  3. 第三个就是正常情况,它会调用到SchemeHostPort::SerializeInternal函数

SchemeHostPort::SerializeInternal

chromium/url/scheme_host_port.cc

std::string SchemeHostPort::SerializeInternal(url::Parsed* parsed) const {
  std::string result;
  if (!IsValid())
    return result;

  // Reserve enough space for the "normal" case of scheme://host/.
  result.reserve(scheme_.size() + host_.size() + 4);

  if (!scheme_.empty()) {
    parsed->scheme = Component(0, scheme_.length());
    result.append(scheme_);
  }

  result.append(kStandardSchemeSeparator);

  if (!host_.empty()) {
    parsed->host = Component(result.length(), host_.length());
    result.append(host_);
  }

  // Omit the port component if the port matches with the default port
  // defined for the scheme, if any.
  int default_port = DefaultPortForScheme(scheme_.data(),
                                          static_cast<int>(scheme_.length()));
  if (default_port == PORT_UNSPECIFIED)
    return result;
  if (port_ != default_port) {
    result.push_back(':');
    std::string port(base::NumberToString(port_));
    parsed->port = Component(result.length(), port.length());
    result.append(std::move(port));
  }

  return result;
}

通过代码可以明确看出来它取了三个东西拼凑在一起scheme、host、port,不过port比较特殊,如果这个scheme的端口号匹配默认的port,就会忽略这个port,比如你的网站是80号端口,他就会忽略

总结

device_id : 源在windows上是设备device path, 加密方式是 HMAC sha256, 盐是一个随机值,可以通过读取Preferences文件(JSON格式)获取,HMAC sha256的key是你最后一个访问的frame origin的root 域名地址

group_id : video的group_id是从audio的gourp id复制而来,具体audio的源没有接着分析,由于靠device_id已经可以解决问题, group_id的加密方式和device id一摸一样, 不过盐是frame_salt,它会根据frame的不同变化