⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 277361 in webkit


Ignore:
Timestamp:
May 12, 2021, 1:05:19 AM (5 years ago)
Author:
youenn@apple.com
Message:

Enumerate AVCaptureDevice list in a background thread
https://bugs.webkit.org/show_bug.cgi?id=225643
<rdar://problem/77820002>

Reviewed by Eric Carlson.

Enumerate AVCaptureDevice in a background queue asynchronously.
Delay getUserMedia and enumerateDevices until this is completed.

Update RealtimeMediaSourceCenter accordingly and introduce RealtimeMediaSourceCenter::enumerateDevices for that purpose.
Replace getCaptureDevices by computeCaptureDevices to do the async computation.
Continue using captureDevices() getters.

Manually tested.

  • platform/mediastream/CaptureDevice.h:

(WebCore::CaptureDevice::CaptureDevice):
(WebCore::CaptureDevice::isolatedCopy):

  • platform/mediastream/CaptureDeviceManager.h:
  • platform/mediastream/RealtimeMediaSourceCenter.cpp:

(WebCore::RealtimeMediaSourceCenter::getMediaStreamDevices):
(WebCore::RealtimeMediaSourceCenter::enumerateDevices):
(WebCore::RealtimeMediaSourceCenter::validateRequestConstraints):
(WebCore::RealtimeMediaSourceCenter::validateRequestConstraintsAfterEnumeration):

  • platform/mediastream/RealtimeMediaSourceCenter.h:
  • platform/mediastream/RealtimeMediaSourceFactory.h:

(WebCore::AudioCaptureFactory::computeSpeakerDevices const):

  • platform/mediastream/ios/AVAudioSessionCaptureDevice.mm:

(WebCore::AVAudioSessionCaptureDevice::AVAudioSessionCaptureDevice):

  • platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.h:
  • platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.mm:

(WebCore::AVAudioSessionCaptureDeviceManager::scheduleUpdateCaptureDevices):
(WebCore::AVAudioSessionCaptureDeviceManager::computeCaptureDevices):

  • platform/mediastream/mac/AVCaptureDeviceManager.h:
  • platform/mediastream/mac/AVCaptureDeviceManager.mm:

(WebCore::AVCaptureDeviceManager::computeCaptureDevices):
(WebCore::AVCaptureDeviceManager::captureDevices):
(WebCore::AVCaptureDeviceManager::updateCachedAVCaptureDevices):
(WebCore::AVCaptureDeviceManager::retrieveCaptureDevices):
(WebCore::AVCaptureDeviceManager::refreshCaptureDevices):
(WebCore::AVCaptureDeviceManager::AVCaptureDeviceManager):

Location:
trunk/Source/WebCore
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r277358 r277361  
     12021-05-12  Youenn Fablet  <youenn@apple.com>
     2
     3        Enumerate AVCaptureDevice list in a background thread
     4        https://bugs.webkit.org/show_bug.cgi?id=225643
     5        <rdar://problem/77820002>
     6
     7        Reviewed by Eric Carlson.
     8
     9        Enumerate AVCaptureDevice in a background queue asynchronously.
     10        Delay getUserMedia and enumerateDevices until this is completed.
     11
     12        Update RealtimeMediaSourceCenter accordingly and introduce RealtimeMediaSourceCenter::enumerateDevices for that purpose.
     13        Replace getCaptureDevices by computeCaptureDevices to do the async computation.
     14        Continue using captureDevices() getters.
     15
     16        Manually tested.
     17
     18        * platform/mediastream/CaptureDevice.h:
     19        (WebCore::CaptureDevice::CaptureDevice):
     20        (WebCore::CaptureDevice::isolatedCopy):
     21        * platform/mediastream/CaptureDeviceManager.h:
     22        * platform/mediastream/RealtimeMediaSourceCenter.cpp:
     23        (WebCore::RealtimeMediaSourceCenter::getMediaStreamDevices):
     24        (WebCore::RealtimeMediaSourceCenter::enumerateDevices):
     25        (WebCore::RealtimeMediaSourceCenter::validateRequestConstraints):
     26        (WebCore::RealtimeMediaSourceCenter::validateRequestConstraintsAfterEnumeration):
     27        * platform/mediastream/RealtimeMediaSourceCenter.h:
     28        * platform/mediastream/RealtimeMediaSourceFactory.h:
     29        (WebCore::AudioCaptureFactory::computeSpeakerDevices const):
     30        * platform/mediastream/ios/AVAudioSessionCaptureDevice.mm:
     31        (WebCore::AVAudioSessionCaptureDevice::AVAudioSessionCaptureDevice):
     32        * platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.h:
     33        * platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.mm:
     34        (WebCore::AVAudioSessionCaptureDeviceManager::scheduleUpdateCaptureDevices):
     35        (WebCore::AVAudioSessionCaptureDeviceManager::computeCaptureDevices):
     36        * platform/mediastream/mac/AVCaptureDeviceManager.h:
     37        * platform/mediastream/mac/AVCaptureDeviceManager.mm:
     38        (WebCore::AVCaptureDeviceManager::computeCaptureDevices):
     39        (WebCore::AVCaptureDeviceManager::captureDevices):
     40        (WebCore::AVCaptureDeviceManager::updateCachedAVCaptureDevices):
     41        (WebCore::AVCaptureDeviceManager::retrieveCaptureDevices):
     42        (WebCore::AVCaptureDeviceManager::refreshCaptureDevices):
     43        (WebCore::AVCaptureDeviceManager::AVCaptureDeviceManager):
     44
    1452021-05-11  Cameron McCormack  <heycam@apple.com>
    246
  • trunk/Source/WebCore/platform/mediastream/CaptureDevice.h

    r275871 r277361  
    3434    enum class DeviceType { Unknown, Microphone, Speaker, Camera, Screen, Window };
    3535
    36     CaptureDevice(const String& persistentId, DeviceType type, const String& label, const String& groupId = emptyString())
     36    CaptureDevice(const String& persistentId, DeviceType type, const String& label, const String& groupId = emptyString(), bool isEnabled = false, bool isDefault = false, bool isMock = false)
    3737        : m_persistentId(persistentId)
    3838        , m_type(type)
    3939        , m_label(label)
    4040        , m_groupId(groupId)
     41        , m_enabled(isEnabled)
     42        , m_default(isDefault)
     43        , m_isMockDevice(isMock)
    4144    {
    4245    }
     
    7174
    7275    explicit operator bool() const { return m_type != DeviceType::Unknown; }
     76
     77    CaptureDevice isolatedCopy() &&;
    7378
    7479#if ENABLE(MEDIA_STREAM)
     
    159164
    160165    return false;
     166}
     167
     168inline CaptureDevice CaptureDevice::isolatedCopy() &&
     169{
     170    return {
     171        WTFMove(m_persistentId).isolatedCopy(),
     172        m_type,
     173        WTFMove(m_label).isolatedCopy(),
     174        WTFMove(m_groupId).isolatedCopy(),
     175        m_enabled,
     176        m_default,
     177        m_isMockDevice
     178    };
    161179}
    162180
  • trunk/Source/WebCore/platform/mediastream/CaptureDeviceManager.h

    r271471 r277361  
    3636public:
    3737    virtual const Vector<CaptureDevice>& captureDevices() = 0;
    38     virtual void getCaptureDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&& callback) { callback(copyToVector(captureDevices())); };
     38    virtual void computeCaptureDevices(CompletionHandler<void()>&& callback) { callback(); }
    3939    virtual Optional<CaptureDevice> captureDeviceWithPersistentID(CaptureDevice::DeviceType, const String&) { return WTF::nullopt; }
    4040
  • trunk/Source/WebCore/platform/mediastream/RealtimeMediaSourceCenter.cpp

    r275871 r277361  
    3939#include "Logging.h"
    4040#include "MediaStreamPrivate.h"
     41#include <wtf/CallbackAggregator.h>
    4142#include <wtf/SHA1.h>
    4243
     
    120121void RealtimeMediaSourceCenter::getMediaStreamDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&& completion)
    121122{
    122     class CaptureDeviceAccumulator : public RefCounted<CaptureDeviceAccumulator> {
    123     public:
    124         static Ref<CaptureDeviceAccumulator> create(CompletionHandler<void(Vector<CaptureDevice>&&)>&& completion)
    125         {
    126             return adoptRef(*new CaptureDeviceAccumulator(WTFMove(completion)));
    127         }
    128 
    129         ~CaptureDeviceAccumulator()
    130         {
    131             m_completionHandler(WTFMove(m_results));
    132         }
    133 
    134         CompletionHandler<void(Vector<CaptureDevice>&&)> accumulate()
    135         {
    136             return [this, protectedThis = makeRef(*this)] (Vector<CaptureDevice>&& result) {
    137                 m_results.appendVector(WTFMove(result));
    138             };
    139         }
    140 
    141     private:
    142         explicit CaptureDeviceAccumulator(CompletionHandler<void(Vector<CaptureDevice>&&)>&& completion)
    143             : m_completionHandler(WTFMove(completion))
    144         {
    145         }
    146 
    147         CompletionHandler<void(Vector<CaptureDevice>&&)> m_completionHandler;
    148         Vector<CaptureDevice> m_results;
    149     };
    150 
    151     auto accumulator = CaptureDeviceAccumulator::create([completion = WTFMove(completion)] (auto&& devices) mutable {
    152         devices.removeAllMatching([] (auto& captureDevice) {
    153             return !captureDevice.enabled();
    154         });
    155         completion(WTFMove(devices));
     123    enumerateDevices(true, true, true, true, [this, completion = WTFMove(completion)]() mutable {
     124        Vector<CaptureDevice> results;
     125
     126        results.appendVector(audioCaptureFactory().audioCaptureDeviceManager().captureDevices());
     127        results.appendVector(videoCaptureFactory().videoCaptureDeviceManager().captureDevices());
     128        results.appendVector(displayCaptureFactory().displayCaptureDeviceManager().captureDevices());
     129        results.appendVector(audioCaptureFactory().speakerDevices());
     130
     131        completion(WTFMove(results));
    156132    });
    157 
    158     audioCaptureFactory().audioCaptureDeviceManager().getCaptureDevices(accumulator->accumulate());
    159     videoCaptureFactory().videoCaptureDeviceManager().getCaptureDevices(accumulator->accumulate());
    160     displayCaptureFactory().displayCaptureDeviceManager().getCaptureDevices(accumulator->accumulate());
    161     audioCaptureFactory().getSpeakerDevices(accumulator->accumulate());
    162133}
    163134
     
    275246}
    276247
     248void RealtimeMediaSourceCenter::enumerateDevices(bool shouldEnumerateCamera, bool shouldEnumerateDisplay, bool shouldEnumerateMicrophone, bool shouldEnumerateSpeakers, CompletionHandler<void()>&& callback)
     249{
     250    auto callbackAggregator = CallbackAggregator::create(WTFMove(callback));
     251    if (shouldEnumerateCamera)
     252        videoCaptureFactory().videoCaptureDeviceManager().computeCaptureDevices([callbackAggregator] { });
     253    if (shouldEnumerateDisplay)
     254        displayCaptureFactory().displayCaptureDeviceManager().computeCaptureDevices([callbackAggregator] { });
     255    if (shouldEnumerateMicrophone)
     256        audioCaptureFactory().audioCaptureDeviceManager().computeCaptureDevices([callbackAggregator] { });
     257    if (shouldEnumerateSpeakers)
     258        audioCaptureFactory().computeSpeakerDevices([callbackAggregator] { });
     259}
     260
    277261void RealtimeMediaSourceCenter::validateRequestConstraints(ValidConstraintsHandler&& validHandler, InvalidConstraintsHandler&& invalidHandler, const MediaStreamRequest& request, String&& deviceIdentifierHashSalt)
     262{
     263    bool shouldEnumerateCamera = request.videoConstraints.isValid;
     264    bool shouldEnumerateDisplay = request.type == MediaStreamRequest::Type::DisplayMedia;
     265    bool shouldEnumerateMicrophone = request.audioConstraints.isValid;
     266    bool shouldEnumerateSpeakers = false;
     267    enumerateDevices(shouldEnumerateCamera, shouldEnumerateDisplay, shouldEnumerateMicrophone, shouldEnumerateSpeakers, [this, validHandler = WTFMove(validHandler), invalidHandler = WTFMove(invalidHandler), request, deviceIdentifierHashSalt = WTFMove(deviceIdentifierHashSalt)]() mutable {
     268        validateRequestConstraintsAfterEnumeration(WTFMove(validHandler), WTFMove(invalidHandler), request, WTFMove(deviceIdentifierHashSalt));
     269    });
     270}
     271
     272void RealtimeMediaSourceCenter::validateRequestConstraintsAfterEnumeration(ValidConstraintsHandler&& validHandler, InvalidConstraintsHandler&& invalidHandler, const MediaStreamRequest& request, String&& deviceIdentifierHashSalt)
    278273{
    279274    struct {
  • trunk/Source/WebCore/platform/mediastream/RealtimeMediaSourceCenter.h

    r275871 r277361  
    116116    void getDisplayMediaDevices(const MediaStreamRequest&, Vector<DeviceInfo>&, String&);
    117117    void getUserMediaDevices(const MediaStreamRequest&, String&&, Vector<DeviceInfo>& audioDevices, Vector<DeviceInfo>& videoDevices, String&);
     118    void validateRequestConstraintsAfterEnumeration(ValidConstraintsHandler&&, InvalidConstraintsHandler&&, const MediaStreamRequest&, String&&);
     119    void enumerateDevices(bool shouldEnumerateCamera, bool shouldEnumerateDisplay, bool shouldEnumerateMicrophone, bool shouldEnumerateSpeakers, CompletionHandler<void()>&&);
    118120
    119121    RealtimeMediaSourceSupportedConstraints m_supportedConstraints;
  • trunk/Source/WebCore/platform/mediastream/RealtimeMediaSourceFactory.h

    r271471 r277361  
    6464    virtual CaptureDeviceManager& audioCaptureDeviceManager() = 0;
    6565    virtual const Vector<CaptureDevice>& speakerDevices() const = 0;
    66     virtual void getSpeakerDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&& completion) const { completion(copyToVector(speakerDevices())); }
     66    virtual void computeSpeakerDevices(CompletionHandler<void()>&& callback) const { callback(); }
    6767
    6868    class ExtensiveObserver : public CanMakeWeakPtr<ExtensiveObserver> { };
  • trunk/Source/WebCore/platform/mediastream/ios/AVAudioSessionCaptureDevice.mm

    r274819 r277361  
    4646
    4747AVAudioSessionCaptureDevice::AVAudioSessionCaptureDevice(const String& persistentId, DeviceType type, const String& label, const String& groupId, bool isEnabled, bool isDefault, bool isMock)
    48     : CaptureDevice(persistentId, type, label, groupId)
     48    : CaptureDevice(persistentId, type, label, groupId, isEnabled, isDefault, isMock)
    4949{
    50     setEnabled(isEnabled);
    51     setIsDefault(isDefault);
    52     setIsMockDevice(isMock);
    5350}
    5451
  • trunk/Source/WebCore/platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.h

    r276880 r277361  
    4848
    4949    const Vector<CaptureDevice>& captureDevices() final;
    50     void getCaptureDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&&) final;
     50    void computeCaptureDevices(CompletionHandler<void()>&&) final;
    5151    const Vector<CaptureDevice>& speakerDevices() const { return m_speakerDevices; }
    5252    Optional<CaptureDevice> captureDeviceWithPersistentID(CaptureDevice::DeviceType, const String&);
  • trunk/Source/WebCore/platform/mediastream/ios/AVAudioSessionCaptureDeviceManager.mm

    r274819 r277361  
    147147void AVAudioSessionCaptureDeviceManager::scheduleUpdateCaptureDevices()
    148148{
    149     getCaptureDevices([] (auto) { });
     149    computeCaptureDevices([] { });
    150150}
    151151
     
    167167}
    168168
    169 void AVAudioSessionCaptureDeviceManager::getCaptureDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&& completion)
     169void AVAudioSessionCaptureDeviceManager::computeCaptureDevices(CompletionHandler<void()>&& completion)
    170170{
    171171    if (m_audioSessionState == AudioSessionState::Inactive) {
     
    181181        callOnWebThreadOrDispatchAsyncOnMainThread(makeBlockPtr([this, completion = WTFMove(completion), newAudioDevices = WTFMove(newAudioDevices).isolatedCopy()] () mutable {
    182182            setAudioCaptureDevices(WTFMove(newAudioDevices));
    183             completion(copyToVector(*m_devices));
     183            completion();
    184184        }).get());
    185185    });
  • trunk/Source/WebCore/platform/mediastream/mac/AVCaptureDeviceManager.h

    r260415 r277361  
    3434#include <wtf/NeverDestroyed.h>
    3535#include <wtf/RetainPtr.h>
     36#include <wtf/WorkQueue.h>
    3637#include <wtf/text/WTFString.h>
    3738
     
    4849    friend class NeverDestroyed<AVCaptureDeviceManager>;
    4950public:
    50     const Vector<CaptureDevice>& captureDevices() final;
    51 
    5251    static AVCaptureDeviceManager& singleton();
    5352
    54     void refreshCaptureDevices();
     53    void refreshCaptureDevices(CompletionHandler<void()>&& = [] { });
    5554
    5655private:
     
    6059    ~AVCaptureDeviceManager() final;
    6160
     61    void computeCaptureDevices(CompletionHandler<void()>&&) final;
     62    const Vector<CaptureDevice>& captureDevices() final;
     63
    6264    void registerForDeviceNotifications();
    63     Vector<CaptureDevice>& captureDevicesInternal();
    6465    void updateCachedAVCaptureDevices();
    65     bool isMatchingExistingCaptureDevice(AVCaptureDevice*);
     66    Vector<CaptureDevice> retrieveCaptureDevices();
    6667
    6768    RetainPtr<WebCoreAVCaptureDeviceManagerObserver> m_objcObserver;
    6869    Vector<CaptureDevice> m_devices;
    6970    RetainPtr<NSMutableArray> m_avCaptureDevices;
    70     bool m_notifyWhenDeviceListChanges { false };
     71    bool m_isInitialized { false };
     72
     73    Ref<WorkQueue> m_dispatchQueue;
    7174};
    7275
  • trunk/Source/WebCore/platform/mediastream/mac/AVCaptureDeviceManager.mm

    r271493 r277361  
    6060namespace WebCore {
    6161
    62 
    63 Vector<CaptureDevice>& AVCaptureDeviceManager::captureDevicesInternal()
    64 {
    65     if (!isAvailable())
    66         return m_devices;
    67 
    68     static bool firstTime = true;
    69     if (firstTime) {
    70         firstTime = false;
    71         refreshCaptureDevices();
    72         m_notifyWhenDeviceListChanges = true;
    73     }
    74 
     62void AVCaptureDeviceManager::computeCaptureDevices(CompletionHandler<void()>&& callback)
     63{
     64    if (!m_isInitialized) {
     65        refreshCaptureDevices([this, callback = WTFMove(callback)]() mutable {
     66            m_isInitialized = true;
     67            callback();
     68        });
     69        return;
     70    }
     71    callback();
     72}
     73
     74const Vector<CaptureDevice>& AVCaptureDeviceManager::captureDevices()
     75{
     76    ASSERT(m_isInitialized);
     77    RELEASE_LOG_ERROR_IF(!m_isInitialized, WebRTC, "Retrieving AVCaptureDeviceManager list before initialization");
    7578    return m_devices;
    76 }
    77 
    78 const Vector<CaptureDevice>& AVCaptureDeviceManager::captureDevices()
    79 {
    80     return captureDevicesInternal();
    8179}
    8280
     
    9694void AVCaptureDeviceManager::updateCachedAVCaptureDevices()
    9795{
     96    ASSERT(!isMainThread());
    9897    auto* currentDevices = [PAL::getAVCaptureDeviceClass() devices];
    9998    auto changedDevices = adoptNS([[NSMutableArray alloc] init]);
     
    130129}
    131130
    132 bool AVCaptureDeviceManager::isMatchingExistingCaptureDevice(AVCaptureDevice *device)
    133 {
    134     auto existingCaptureDevice = captureDeviceFromPersistentID(device.uniqueID);
    135     if (!existingCaptureDevice)
    136         return false;
    137 
    138     return deviceIsAvailable(device) == existingCaptureDevice.enabled();
    139 }
    140 
    141 static inline bool isDefaultVideoCaptureDeviceFirst(const Vector<CaptureDevice>& devices, const String& defaultDeviceID)
    142 {
    143     if (devices.isEmpty())
    144         return false;
    145     return devices[0].persistentId() == defaultDeviceID;
    146 }
    147 
    148131static inline bool isVideoDevice(AVCaptureDevice *device)
    149132{
     
    151134}
    152135
    153 void AVCaptureDeviceManager::refreshCaptureDevices()
    154 {
     136Vector<CaptureDevice> AVCaptureDeviceManager::retrieveCaptureDevices()
     137{
     138    ASSERT(!isMainThread());
     139    if (!isAvailable())
     140        return { };
     141
    155142    if (!m_avCaptureDevices) {
    156143        m_avCaptureDevices = adoptNS([[NSMutableArray alloc] init]);
     
    179166#endif
    180167
    181     bool deviceHasChanged = false;
    182168    if (defaultVideoDevice) {
    183         deviceList.append(toCaptureDevice(defaultVideoDevice));
    184         deviceHasChanged = !isDefaultVideoCaptureDeviceFirst(captureDevices(), defaultVideoDevice.uniqueID);
     169        auto device = toCaptureDevice(defaultVideoDevice);
     170        device.setIsDefault(true);
     171        deviceList.append(WTFMove(device));
    185172    }
    186173    for (AVCaptureDevice *platformDevice in currentDevices) {
    187         if (!isVideoDevice(platformDevice))
    188             continue;
    189 
    190         if (!deviceHasChanged && !isMatchingExistingCaptureDevice(platformDevice))
    191             deviceHasChanged = true;
    192 
    193         if (platformDevice.uniqueID == defaultVideoDevice.uniqueID)
    194             continue;
    195 
    196         deviceList.append(toCaptureDevice(platformDevice));
    197     }
    198 
    199     if (deviceHasChanged || m_devices.size() != deviceList.size()) {
    200         deviceHasChanged = true;
    201         m_devices = WTFMove(deviceList);
    202     }
    203 
    204     if (m_notifyWhenDeviceListChanges && deviceHasChanged)
    205         deviceChanged();
     174        if (isVideoDevice(platformDevice) && platformDevice.uniqueID != defaultVideoDevice.uniqueID)
     175            deviceList.append(toCaptureDevice(platformDevice));
     176    }
     177    return deviceList;
     178}
     179
     180void AVCaptureDeviceManager::refreshCaptureDevices(CompletionHandler<void()>&& callback)
     181{
     182    m_dispatchQueue->dispatch([this, callback = WTFMove(callback)]() mutable {
     183        RunLoop::main().dispatch([this, callback = WTFMove(callback), deviceList = retrieveCaptureDevices().isolatedCopy()]() mutable {           
     184            bool deviceHasChanged = m_devices.size() != deviceList.size();
     185            if (!deviceHasChanged) {
     186                for (size_t cptr = 0; cptr < deviceList.size(); ++cptr) {
     187                    if (m_devices[cptr].persistentId() != deviceList[cptr].persistentId() || m_devices[cptr].enabled() != deviceList[cptr].enabled()) {
     188                        deviceHasChanged = true;
     189                        break;
     190                    }
     191                }
     192            }
     193
     194            if (deviceHasChanged) {
     195                m_devices = WTFMove(deviceList);
     196                if (m_isInitialized)
     197                    deviceChanged();
     198            }
     199            callback();
     200        });
     201    });
    206202}
    207203
     
    219215AVCaptureDeviceManager::AVCaptureDeviceManager()
    220216    : m_objcObserver(adoptNS([[WebCoreAVCaptureDeviceManagerObserver alloc] initWithCallback: this]))
     217    , m_dispatchQueue(WorkQueue::create("com.apple.WebKit.AVCaptureDeviceManager"))
    221218{
    222219}
Note: See TracChangeset for help on using the changeset viewer.