| /* |
| * Copyright (C) 2018 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. |
| */ |
| |
| WI.MediaTimelineRecord = class MediaTimelineRecord extends WI.TimelineRecord |
| { |
| constructor(eventType, domNodeOrInfo, {trackingAnimationId, animationName, transitionProperty} = {}) |
| { |
| console.assert(Object.values(MediaTimelineRecord.EventType).includes(eventType)); |
| console.assert(domNodeOrInfo instanceof WI.DOMNode || (!isEmptyObject(domNodeOrInfo) && domNodeOrInfo.displayName && domNodeOrInfo.cssPath)); |
| |
| super(WI.TimelineRecord.Type.Media); |
| |
| this._eventType = eventType; |
| this._domNode = domNodeOrInfo; |
| this._domNodeDisplayName = domNodeOrInfo?.displayName; |
| this._domNodeCSSPath = domNodeOrInfo instanceof WI.DOMNode ? WI.cssPath(domNodeOrInfo, {full: true}) : domNodeOrInfo?.cssPath; |
| |
| // Web Animation |
| console.assert(trackingAnimationId === undefined || typeof trackingAnimationId === "string"); |
| this._trackingAnimationId = trackingAnimationId || null; |
| |
| // CSS Web Animation |
| console.assert(animationName === undefined || typeof animationName === "string"); |
| console.assert(transitionProperty === undefined || typeof transitionProperty === "string"); |
| this._animationName = animationName || null; |
| this._transitionProperty = transitionProperty || null; |
| |
| this._timestamps = []; |
| this._activeStartTime = NaN; |
| } |
| |
| // Import / Export |
| |
| static async fromJSON(json) |
| { |
| let {eventType, domNodeDisplayName, domNodeCSSPath, animationName, transitionProperty, timestamps} = json; |
| |
| let documentNode = null; |
| if (InspectorBackend.hasDomain("DOM")) |
| documentNode = await new Promise((resolve) => WI.domManager.requestDocument(resolve)); |
| |
| let domNode = null; |
| if (documentNode && domNodeCSSPath) { |
| try { |
| let nodeId = await documentNode.querySelector(domNodeCSSPath); |
| if (nodeId) |
| domNode = WI.domManager.nodeForId(nodeId); |
| } catch { } |
| } |
| if (!domNode) { |
| domNode = { |
| displayName: domNodeDisplayName, |
| cssPath: domNodeCSSPath, |
| }; |
| } |
| |
| let record = new MediaTimelineRecord(eventType, domNode, {animationName, transitionProperty}); |
| |
| if (Array.isArray(timestamps) && timestamps.length) { |
| record._timestamps = []; |
| for (let item of timestamps) { |
| if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent) { |
| if (documentNode && item.originatorCSSPath) { |
| try { |
| let nodeId = await documentNode.querySelector(item.originatorCSSPath); |
| if (nodeId) |
| item.originator = WI.domManager.nodeForId(nodeId); |
| } catch { } |
| if (!item.originator) { |
| item.originator = { |
| displayName: item.originatorDisplayName, |
| cssPath: item.originatorCSSPath, |
| }; |
| } |
| } |
| } |
| record._timestamps.push(item); |
| } |
| } |
| |
| return record; |
| } |
| |
| toJSON() |
| { |
| let json = { |
| eventType: this._eventType, |
| domNodeDisplayName: this._domNodeDisplayName, |
| domNodeCSSPath: this._domNodeCSSPath, |
| }; |
| |
| if (this._animationName) |
| json.animationName = this._animationName; |
| |
| if (this._transitionProperty) |
| json.transitionProperty = this._transitionProperty; |
| |
| if (this._timestamps.length) { |
| json.timestamps = this._timestamps.map((item) => { |
| if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent && item.originator instanceof WI.DOMNode) |
| delete item.originator; |
| return item; |
| }); |
| } |
| |
| return json; |
| } |
| |
| // Public |
| |
| get eventType() { return this._eventType; } |
| get domNode() { return this._domNode; } |
| get trackingAnimationId() { return this._trackingAnimationId; } |
| get timestamps() { return this._timestamps; } |
| get activeStartTime() { return this._activeStartTime; } |
| |
| get updatesDynamically() |
| { |
| return true; |
| } |
| |
| get usesActiveStartTime() |
| { |
| return true; |
| } |
| |
| get displayName() |
| { |
| switch (this._eventType) { |
| case MediaTimelineRecord.EventType.CSSAnimation: |
| return this._animationName; |
| |
| case MediaTimelineRecord.EventType.CSSTransition: |
| return this._transitionProperty; |
| |
| case MediaTimelineRecord.EventType.MediaElement: |
| return WI.UIString("Media Element"); |
| } |
| |
| console.error("Unknown media record event type: ", this._eventType, this); |
| return WI.UIString("Media Event"); |
| } |
| |
| get subtitle() |
| { |
| switch (this._eventType) { |
| case MediaTimelineRecord.EventType.CSSAnimation: |
| return WI.UIString("CSS Animation"); |
| |
| case MediaTimelineRecord.EventType.CSSTransition: |
| return WI.UIString("CSS Transition"); |
| } |
| |
| return ""; |
| } |
| |
| saveIdentityToCookie(cookie) |
| { |
| super.saveIdentityToCookie(cookie); |
| |
| cookie["media-timeline-record-event-type"] = this._eventType; |
| cookie["media-timeline-record-dom-node"] = this._domNode instanceof WI.DOMNode ? this._domNode.path() : this._domNode; |
| if (this._animationName) |
| cookie["media-timeline-record-animation-name"] = this._animationName; |
| if (this._transitionProperty) |
| cookie["media-timeline-record-transition-property"] = this._transitionProperty; |
| } |
| |
| // TimelineManager |
| |
| updateAnimationState(timestamp, animationState) |
| { |
| console.assert(this._eventType === MediaTimelineRecord.EventType.CSSAnimation || this._eventType === MediaTimelineRecord.EventType.CSSTransition); |
| console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp); |
| |
| let type; |
| switch (animationState) { |
| case InspectorBackend.Enum.Animation.AnimationState.Ready: |
| type = MediaTimelineRecord.TimestampType.CSSAnimationReady; |
| break; |
| case InspectorBackend.Enum.Animation.AnimationState.Delayed: |
| type = MediaTimelineRecord.TimestampType.CSSAnimationDelay; |
| break; |
| case InspectorBackend.Enum.Animation.AnimationState.Active: |
| type = MediaTimelineRecord.TimestampType.CSSAnimationActive; |
| break; |
| case InspectorBackend.Enum.Animation.AnimationState.Canceled: |
| type = MediaTimelineRecord.TimestampType.CSSAnimationCancel; |
| break; |
| case InspectorBackend.Enum.Animation.AnimationState.Done: |
| type = MediaTimelineRecord.TimestampType.CSSAnimationDone; |
| break; |
| } |
| console.assert(type); |
| |
| this._timestamps.push({type, timestamp}); |
| |
| this._updateTimes(); |
| } |
| |
| addDOMEvent(timestamp, domEvent) |
| { |
| console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement); |
| console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp); |
| |
| let data = { |
| type: MediaTimelineRecord.TimestampType.MediaElementDOMEvent, |
| timestamp, |
| eventName: domEvent.eventName, |
| }; |
| if (domEvent.originator instanceof WI.DOMNode) { |
| data.originator = domEvent.originator; |
| data.originatorDisplayName = data.originator.displayName; |
| data.originatorCSSPath = WI.cssPath(data.originator, {full: true}); |
| } |
| if (!isEmptyObject(domEvent.data)) |
| data.data = domEvent.data; |
| this._timestamps.push(data); |
| |
| this._updateTimes(); |
| } |
| |
| powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient) |
| { |
| console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement); |
| console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp); |
| |
| this._timestamps.push({ |
| type: MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange, |
| timestamp, |
| isPowerEfficient, |
| }); |
| |
| this._updateTimes(); |
| } |
| |
| // Private |
| |
| _updateTimes() |
| { |
| let oldStartTime = this.startTime; |
| let oldEndTime = this.endTime; |
| |
| let firstItem = this._timestamps[0]; |
| let lastItem = this._timestamps.lastValue; |
| |
| if (isNaN(this._startTime)) |
| this._startTime = firstItem.timestamp; |
| |
| if (isNaN(this._activeStartTime)) { |
| if (this._eventType === MediaTimelineRecord.EventType.MediaElement) |
| this._activeStartTime = firstItem.timestamp; |
| else if (firstItem.type === MediaTimelineRecord.TimestampType.CSSAnimationActive) |
| this._activeStartTime = firstItem.timestamp; |
| } |
| |
| switch (lastItem.type) { |
| case MediaTimelineRecord.TimestampType.CSSAnimationCancel: |
| case MediaTimelineRecord.TimestampType.CSSAnimationDone: |
| this._endTime = lastItem.timestamp; |
| break; |
| |
| case MediaTimelineRecord.TimestampType.MediaElementDOMEvent: |
| if (WI.DOMNode.isPlayEvent(lastItem.eventName)) |
| this._endTime = NaN; |
| else if (!isNaN(this._endTime) || WI.DOMNode.isPauseEvent(lastItem.eventName) || WI.DOMNode.isStopEvent(lastItem.eventName)) |
| this._endTime = lastItem.timestamp; |
| break; |
| } |
| |
| if (this.startTime !== oldStartTime || this.endTime !== oldEndTime) |
| this.dispatchEventToListeners(WI.TimelineRecord.Event.Updated); |
| } |
| }; |
| |
| WI.MediaTimelineRecord.EventType = { |
| CSSAnimation: "css-animation", |
| CSSTransition: "css-transition", |
| MediaElement: "media-element", |
| }; |
| |
| WI.MediaTimelineRecord.TimestampType = { |
| CSSAnimationReady: "css-animation-ready", |
| CSSAnimationDelay: "css-animation-delay", |
| CSSAnimationActive: "css-animation-active", |
| CSSAnimationCancel: "css-animation-cancel", |
| CSSAnimationDone: "css-animation-done", |
| // CSS transitions share the same timestamp types. |
| |
| MediaElementDOMEvent: "media-element-dom-event", |
| MediaElementPowerEfficientPlaybackStateChange: "media-element-power-efficient-playback-state-change", |
| }; |