diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js')
-rw-r--r-- | Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js new file mode 100644 index 000000000..ab04ddf10 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2013 University of Washington. All rights reserved. + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.ReplayManager = class ReplayManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._sessionState = WebInspector.ReplayManager.SessionState.Inactive; + this._segmentState = WebInspector.ReplayManager.SegmentState.Unloaded; + + this._activeSessionIdentifier = null; + this._activeSegmentIdentifier = null; + this._currentPosition = new WebInspector.ReplayPosition(0, 0); + this._initialized = false; + + // These hold actual instances of sessions and segments. + this._sessions = new Map; + this._segments = new Map; + // These hold promises that resolve when the instance data is recieved. + this._sessionPromises = new Map; + this._segmentPromises = new Map; + + // Playback speed is specified in replayToPosition commands, and persists + // for the duration of the playback command until another playback begins. + this._playbackSpeed = WebInspector.ReplayManager.PlaybackSpeed.RealTime; + + if (window.ReplayAgent) { + var instance = this; + this._initializationPromise = ReplayAgent.currentReplayState() + .then(function(payload) { + console.assert(payload.sessionState in WebInspector.ReplayManager.SessionState, "Unknown session state: " + payload.sessionState); + console.assert(payload.segmentState in WebInspector.ReplayManager.SegmentState, "Unknown segment state: " + payload.segmentState); + + instance._activeSessionIdentifier = payload.sessionIdentifier; + instance._activeSegmentIdentifier = payload.segmentIdentifier; + instance._sessionState = WebInspector.ReplayManager.SessionState[payload.sessionState]; + instance._segmentState = WebInspector.ReplayManager.SegmentState[payload.segmentState]; + instance._currentPosition = payload.replayPosition; + + instance._initialized = true; + }).then(function() { + return ReplayAgent.getAvailableSessions(); + }).then(function(payload) { + for (var sessionId of payload.ids) + instance.sessionCreated(sessionId); + }).catch(function(error) { + console.error("ReplayManager initialization failed: ", error); + throw error; + }); + } + } + + // Public + + // The following state is invalid unless called from a function that's chained + // to the (resolved) ReplayManager.waitUntilInitialized promise. + get sessionState() + { + console.assert(this._initialized); + return this._sessionState; + } + + get segmentState() + { + console.assert(this._initialized); + return this._segmentState; + } + + get activeSessionIdentifier() + { + console.assert(this._initialized); + return this._activeSessionIdentifier; + } + + get activeSegmentIdentifier() + { + console.assert(this._initialized); + return this._activeSegmentIdentifier; + } + + get playbackSpeed() + { + console.assert(this._initialized); + return this._playbackSpeed; + } + + set playbackSpeed(value) + { + console.assert(this._initialized); + this._playbackSpeed = value; + } + + get currentPosition() + { + console.assert(this._initialized); + return this._currentPosition; + } + + // These return promises even if the relevant instance is already created. + waitUntilInitialized() // --> () + { + return this._initializationPromise; + } + + // Return a promise that resolves to a session, if it exists. + getSession(sessionId) // --> (WebInspector.ReplaySession) + { + if (this._sessionPromises.has(sessionId)) + return this._sessionPromises.get(sessionId); + + var newPromise = ReplayAgent.getSessionData(sessionId) + .then(function(payload) { + return Promise.resolve(WebInspector.ReplaySession.fromPayload(sessionId, payload)); + }); + + this._sessionPromises.set(sessionId, newPromise); + return newPromise; + } + + // Return a promise that resolves to a session segment, if it exists. + getSegment(segmentId) // --> (WebInspector.ReplaySessionSegment) + { + if (this._segmentPromises.has(segmentId)) + return this._segmentPromises.get(segmentId); + + var newPromise = ReplayAgent.getSegmentData(segmentId) + .then(function(payload) { + return Promise.resolve(new WebInspector.ReplaySessionSegment(segmentId, payload)); + }); + + this._segmentPromises.set(segmentId, newPromise); + return newPromise; + } + + // Switch to the specified session. + // Returns a promise that resolves when the switch completes. + switchSession(sessionId) // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) { + result = result.then(function() { + return WebInspector.replayManager.cancelPlayback(); + }); + } + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return manager.getSession(sessionId); + }).then(function ensureSessionDataIsLoaded(session) { + return ReplayAgent.switchSession(session.identifier); + }).catch(function(error) { + console.error("Failed to switch to session: ", error); + throw error; + }); + + return result; + } + + // Start capturing into the current session as soon as possible. + // Returns a promise that resolves when capturing begins. + startCapturing() // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) + return result; // Already capturing. + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) { + result = result.then(function() { + return WebInspector.replayManager.cancelPlayback(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return ReplayAgent.startCapturing(); + }).catch(function(error) { + console.error("Failed to start capturing: ", error); + throw error; + }); + + return result; + } + + // Stop capturing into the current session as soon as possible. + // Returns a promise that resolves when capturing ends. + stopCapturing() // --> () + { + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop capturing unless capture is active."); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.stopCapturing() + .catch(function(error) { + console.error("Failed to stop capturing: ", error); + throw error; + }); + } + + // Pause playback as soon as possible. + // Returns a promise that resolves when playback is paused. + pausePlayback() // --> () + { + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot pause playback while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + return result; // Already stopped. + + if (this.segmentState !== WebInspector.ReplayManager.SegmentState.Dispatching) + return result; // Already stopped. + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching); + + return ReplayAgent.pausePlayback(); + }).catch(function(error) { + console.error("Failed to pause playback: ", error); + throw error; + }); + + return result; + } + + // Pause playback and unload the current session segment as soon as possible. + // Returns a promise that resolves when the current segment is unloaded. + cancelPlayback() // --> () + { + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop playback while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + return result; // Already stopped. + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.cancelPlayback(); + }).catch(function(error) { + console.error("Failed to stop playback: ", error); + throw error; + }); + + return result; + } + + // Replay to the specified position as soon as possible using the current replay speed. + // Returns a promise that resolves when replay has begun (NOT when the position is reached). + replayToPosition(replayPosition) // --> () + { + console.assert(replayPosition instanceof WebInspector.ReplayPosition, "Cannot replay to a position while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.replayToPosition(replayPosition, manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward); + }).catch(function(error) { + console.error("Failed to start playback to position: ", replayPosition, error); + throw error; + }); + + return result; + } + + // Replay to the end of the session as soon as possible using the current replay speed. + // Returns a promise that resolves when replay has begun (NOT when the end is reached). + replayToCompletion() // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching) + return result; // Already running. + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Loaded || manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return ReplayAgent.replayToCompletion(manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward); + }).catch(function(error) { + console.error("Failed to start playback to completion: ", error); + throw error; + }); + + return result; + } + + // Protected (called by ReplayObserver) + + // Since these methods update session and segment state, they depend on the manager + // being properly initialized. So, each function body is prepended with a retry guard. + // This makes call sites simpler and avoids an extra event loop turn in the common case. + + captureStarted() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.captureStarted.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Capturing); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStarted); + } + + captureStopped() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.captureStopped.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStopped); + } + + playbackStarted() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackStarted.bind(this)); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + this._changeSessionState(WebInspector.ReplayManager.SessionState.Replaying); + + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Dispatching); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackStarted); + } + + playbackHitPosition(replayPosition, timestamp) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackHitPosition.bind(this, replayPosition, timestamp)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching); + console.assert(replayPosition instanceof WebInspector.ReplayPosition); + + this._currentPosition = replayPosition; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPositionChanged); + } + + playbackPaused(position) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackPaused.bind(this, position)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPaused); + } + + playbackFinished() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackFinished.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackFinished); + } + + sessionCreated(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionCreated.bind(this, sessionId)); + + console.assert(!this._sessions.has(sessionId), "Tried to add duplicate session identifier:", sessionId); + var sessionMap = this._sessions; + this.getSession(sessionId) + .then(function(session) { + sessionMap.set(sessionId, session); + }).catch(function(error) { + console.error("Error obtaining session data: ", error); + throw error; + }); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionAdded, {sessionId}); + } + + sessionModified(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionModified.bind(this, sessionId)); + + this.getSession(sessionId).then(function(session) { + session.segmentsChanged(); + }); + } + + sessionRemoved(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionRemoved.bind(this, sessionId)); + + console.assert(this._sessions.has(sessionId), "Unknown session identifier:", sessionId); + + if (!this._sessionPromises.has(sessionId)) + return; + + var manager = this; + + this.getSession(sessionId) + .catch(function(error) { + // Wait for any outstanding promise to settle so it doesn't get re-added. + }).then(function() { + manager._sessionPromises.delete(sessionId); + var removedSession = manager._sessions.take(sessionId); + console.assert(removedSession); + manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionRemoved, {removedSession}); + }); + } + + segmentCreated(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentCreated.bind(this, segmentId)); + + console.assert(!this._segments.has(segmentId), "Tried to add duplicate segment identifier:", segmentId); + + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Appending); + + // Create a dummy segment, and don't try to load any data for it. It will + // be removed once the segment is complete, and then its data will be fetched. + var incompleteSegment = new WebInspector.IncompleteSessionSegment(segmentId); + this._segments.set(segmentId, incompleteSegment); + this._segmentPromises.set(segmentId, Promise.resolve(incompleteSegment)); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentAdded, {segmentIdentifier: segmentId}); + } + + segmentCompleted(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentCompleted.bind(this, segmentId)); + + var placeholderSegment = this._segments.take(segmentId); + console.assert(placeholderSegment instanceof WebInspector.IncompleteSessionSegment); + this._segmentPromises.delete(segmentId); + + var segmentMap = this._segments; + this.getSegment(segmentId) + .then(function(segment) { + segmentMap.set(segmentId, segment); + }).catch(function(error) { + console.error("Error obtaining segment data: ", error); + throw error; + }); + } + + segmentRemoved(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentRemoved.bind(this, segmentId)); + + console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId); + + if (!this._segmentPromises.has(segmentId)) + return; + + var manager = this; + + // Wait for any outstanding promise to settle so it doesn't get re-added. + this.getSegment(segmentId) + .catch(function(error) { + return Promise.resolve(); + }).then(function() { + manager._segmentPromises.delete(segmentId); + var removedSegment = manager._segments.take(segmentId); + console.assert(removedSegment); + manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentRemoved, {removedSegment}); + }); + } + + segmentLoaded(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentLoaded.bind(this, segmentId)); + + console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId); + + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded); + + var previousIdentifier = this._activeSegmentIdentifier; + this._activeSegmentIdentifier = segmentId; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier}); + } + + segmentUnloaded() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentUnloaded.bind(this)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded); + + var previousIdentifier = this._activeSegmentIdentifier; + this._activeSegmentIdentifier = null; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier}); + } + + // Private + + _changeSessionState(newState) + { + // Warn about no-op state changes. We shouldn't be seeing them. + var isAllowed = this._sessionState !== newState; + + switch (this._sessionState) { + case WebInspector.ReplayManager.SessionState.Capturing: + isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive; + break; + + case WebInspector.ReplayManager.SessionState.Replaying: + isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive; + break; + } + + console.assert(isAllowed, "Invalid session state change: ", this._sessionState, " to ", newState); + if (isAllowed) + this._sessionState = newState; + } + + _changeSegmentState(newState) + { + // Warn about no-op state changes. We shouldn't be seeing them. + var isAllowed = this._segmentState !== newState; + + switch (this._segmentState) { + case WebInspector.ReplayManager.SegmentState.Appending: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded; + break; + case WebInspector.ReplayManager.SegmentState.Unloaded: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Appending || newState === WebInspector.ReplayManager.SegmentState.Loaded; + break; + case WebInspector.ReplayManager.SegmentState.Loaded: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded || newState === WebInspector.ReplayManager.SegmentState.Dispatching; + break; + case WebInspector.ReplayManager.SegmentState.Dispatching: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Loaded; + break; + } + + console.assert(isAllowed, "Invalid segment state change: ", this._segmentState, " to ", newState); + if (isAllowed) + this._segmentState = newState; + } + + _suppressBreakpointsAndResumeIfNeeded() + { + var manager = this; + + return new Promise(function(resolve, reject) { + manager._breakpointsWereSuppressed = WebInspector.debuggerManager.breakpointsEnabled; + WebInspector.debuggerManager.breakpointsEnabled = false; + + return WebInspector.debuggerManager.resume(); + }); + } +}; + +WebInspector.ReplayManager.Event = { + CaptureStarted: "replay-manager-capture-started", + CaptureStopped: "replay-manager-capture-stopped", + + PlaybackStarted: "replay-manager-playback-started", + PlaybackPaused: "replay-manager-playback-paused", + PlaybackFinished: "replay-manager-playback-finished", + PlaybackPositionChanged: "replay-manager-play-back-position-changed", + + ActiveSessionChanged: "replay-manager-active-session-changed", + ActiveSegmentChanged: "replay-manager-active-segment-changed", + + SessionSegmentAdded: "replay-manager-session-segment-added", + SessionSegmentRemoved: "replay-manager-session-segment-removed", + + SessionAdded: "replay-manager-session-added", + SessionRemoved: "replay-manager-session-removed", +}; + +WebInspector.ReplayManager.SessionState = { + Capturing: "replay-manager-session-state-capturing", + Inactive: "replay-manager-session-state-inactive", + Replaying: "replay-manager-session-state-replaying", +}; + +WebInspector.ReplayManager.SegmentState = { + Appending: "replay-manager-segment-state-appending", + Unloaded: "replay-manager-segment-state-unloaded", + Loaded: "replay-manager-segment-state-loaded", + Dispatching: "replay-manager-segment-state-dispatching", +}; + +WebInspector.ReplayManager.PlaybackSpeed = { + RealTime: "replay-manager-playback-speed-real-time", + FastForward: "replay-manager-playback-speed-fast-forward", +}; |