Keyframe animation doesn't 't show up in the Animations timeline
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / MediaTimelineRecord.js
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.MediaTimelineRecord = class MediaTimelineRecord extends WI.TimelineRecord
27 {
28     constructor(eventType, domNodeOrInfo, {trackingAnimationId, animationName, transitionProperty} = {})
29     {
30         console.assert(Object.values(MediaTimelineRecord.EventType).includes(eventType));
31         console.assert(domNodeOrInfo instanceof WI.DOMNode || (!isEmptyObject(domNodeOrInfo) && domNodeOrInfo.displayName && domNodeOrInfo.cssPath));
32
33         super(WI.TimelineRecord.Type.Media);
34
35         this._eventType = eventType;
36         this._domNode = domNodeOrInfo;
37         this._domNodeDisplayName = domNodeOrInfo?.displayName;
38         this._domNodeCSSPath = domNodeOrInfo instanceof WI.DOMNode ? WI.cssPath(domNodeOrInfo, {full: true}) : domNodeOrInfo?.cssPath;
39
40         // Web Animation
41         console.assert(trackingAnimationId === undefined || typeof trackingAnimationId === "string");
42         this._trackingAnimationId = trackingAnimationId || null;
43
44         // CSS Web Animation
45         console.assert(animationName === undefined || typeof animationName === "string");
46         console.assert(transitionProperty === undefined || typeof transitionProperty === "string");
47         this._animationName = animationName || null;
48         this._transitionProperty = transitionProperty || null;
49
50         this._timestamps = [];
51         this._activeStartTime = NaN;
52     }
53
54     // Import / Export
55
56     static async fromJSON(json)
57     {
58         let {eventType, domNodeDisplayName, domNodeCSSPath, animationName, transitionProperty, timestamps} = json;
59
60         let documentNode = null;
61         if (InspectorBackend.hasDomain("DOM"))
62             documentNode = await new Promise((resolve) => WI.domManager.requestDocument(resolve));
63
64         let domNode = null;
65         if (documentNode && domNodeCSSPath) {
66             try {
67                 let nodeId = await documentNode.querySelector(domNodeCSSPath);
68                 if (nodeId)
69                     domNode = WI.domManager.nodeForId(nodeId);
70             } catch { }
71         }
72         if (!domNode) {
73             domNode = {
74                 displayName: domNodeDisplayName,
75                 cssPath: domNodeCSSPath,
76             };
77         }
78
79         let record = new MediaTimelineRecord(eventType, domNode, {animationName, transitionProperty});
80
81         if (Array.isArray(timestamps) && timestamps.length) {
82             record._timestamps = [];
83             for (let item of timestamps) {
84                 if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent) {
85                     if (documentNode && item.originatorCSSPath) {
86                         try {
87                             let nodeId = await documentNode.querySelector(item.originatorCSSPath);
88                             if (nodeId)
89                                 item.originator = WI.domManager.nodeForId(nodeId);
90                         } catch { }
91                         if (!item.originator) {
92                             item.originator = {
93                                 displayName: item.originatorDisplayName,
94                                 cssPath: item.originatorCSSPath,
95                             };
96                         }
97                     }
98                 }
99                 record._timestamps.push(item);
100             }
101         }
102
103         return record;
104     }
105
106     toJSON()
107     {
108         let json = {
109             eventType: this._eventType,
110             domNodeDisplayName: this._domNodeDisplayName,
111             domNodeCSSPath: this._domNodeCSSPath,
112         };
113
114         if (this._animationName)
115             json.animationName = this._animationName;
116
117         if (this._transitionProperty)
118             json.transitionProperty = this._transitionProperty;
119
120         if (this._timestamps.length) {
121             json.timestamps = this._timestamps.map((item) => {
122                 if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent && item.originator instanceof WI.DOMNode)
123                     delete item.originator;
124                 return item;
125             });
126         }
127
128         return json;
129     }
130
131     // Public
132
133     get eventType() { return this._eventType; }
134     get domNode() { return this._domNode; }
135     get trackingAnimationId() { return this._trackingAnimationId; }
136     get timestamps() { return this._timestamps; }
137     get activeStartTime() { return this._activeStartTime; }
138
139     get updatesDynamically()
140     {
141         return true;
142     }
143
144     get usesActiveStartTime()
145     {
146         return true;
147     }
148
149     get displayName()
150     {
151         switch (this._eventType) {
152         case MediaTimelineRecord.EventType.CSSAnimation:
153             return this._animationName;
154
155         case MediaTimelineRecord.EventType.CSSTransition:
156             return this._transitionProperty;
157
158         case MediaTimelineRecord.EventType.MediaElement:
159             return WI.UIString("Media Element");
160         }
161
162         console.error("Unknown media record event type: ", this._eventType, this);
163         return WI.UIString("Media Event");
164     }
165
166     get subtitle()
167     {
168         switch (this._eventType) {
169         case MediaTimelineRecord.EventType.CSSAnimation:
170             return WI.UIString("CSS Animation");
171
172         case MediaTimelineRecord.EventType.CSSTransition:
173             return WI.UIString("CSS Transition");
174         }
175
176         return "";
177     }
178
179     saveIdentityToCookie(cookie)
180     {
181         super.saveIdentityToCookie(cookie);
182
183         cookie["media-timeline-record-event-type"] = this._eventType;
184         cookie["media-timeline-record-dom-node"] = this._domNode instanceof WI.DOMNode ? this._domNode.path() : this._domNode;
185         if (this._animationName)
186             cookie["media-timeline-record-animation-name"] = this._animationName;
187         if (this._transitionProperty)
188             cookie["media-timeline-record-transition-property"] = this._transitionProperty;
189     }
190
191     // TimelineManager
192
193     updateAnimationState(timestamp, animationState)
194     {
195         console.assert(this._eventType === MediaTimelineRecord.EventType.CSSAnimation || this._eventType === MediaTimelineRecord.EventType.CSSTransition);
196         console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
197
198         let type;
199         switch (animationState) {
200         case InspectorBackend.Enum.Animation.AnimationState.Ready:
201             type = MediaTimelineRecord.TimestampType.CSSAnimationReady;
202             break;
203         case InspectorBackend.Enum.Animation.AnimationState.Delayed:
204             type = MediaTimelineRecord.TimestampType.CSSAnimationDelay;
205             break;
206         case InspectorBackend.Enum.Animation.AnimationState.Active:
207             type = MediaTimelineRecord.TimestampType.CSSAnimationActive;
208             break;
209         case InspectorBackend.Enum.Animation.AnimationState.Canceled:
210             type = MediaTimelineRecord.TimestampType.CSSAnimationCancel;
211             break;
212         case InspectorBackend.Enum.Animation.AnimationState.Done:
213             type = MediaTimelineRecord.TimestampType.CSSAnimationDone;
214             break;
215         }
216         console.assert(type);
217
218         this._timestamps.push({type, timestamp});
219
220         this._updateTimes();
221     }
222
223     addDOMEvent(timestamp, domEvent)
224     {
225         console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
226         console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
227
228         let data = {
229             type: MediaTimelineRecord.TimestampType.MediaElementDOMEvent,
230             timestamp,
231             eventName: domEvent.eventName,
232         };
233         if (domEvent.originator instanceof WI.DOMNode) {
234             data.originator = domEvent.originator;
235             data.originatorDisplayName = data.originator.displayName;
236             data.originatorCSSPath = WI.cssPath(data.originator, {full: true});
237         }
238         if (!isEmptyObject(domEvent.data))
239             data.data = domEvent.data;
240         this._timestamps.push(data);
241
242         this._updateTimes();
243     }
244
245     powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient)
246     {
247         console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
248         console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
249
250         this._timestamps.push({
251             type: MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange,
252             timestamp,
253             isPowerEfficient,
254         });
255
256         this._updateTimes();
257     }
258
259     // Private
260
261     _updateTimes()
262     {
263         let oldStartTime = this.startTime;
264         let oldEndTime = this.endTime;
265
266         let firstItem = this._timestamps[0];
267         let lastItem = this._timestamps.lastValue;
268
269         if (isNaN(this._startTime))
270             this._startTime = firstItem.timestamp;
271
272         if (isNaN(this._activeStartTime)) {
273             if (this._eventType === MediaTimelineRecord.EventType.MediaElement)
274                 this._activeStartTime = firstItem.timestamp;
275             else if (firstItem.type === MediaTimelineRecord.TimestampType.CSSAnimationActive)
276                 this._activeStartTime = firstItem.timestamp;
277         }
278
279         switch (lastItem.type) {
280         case MediaTimelineRecord.TimestampType.CSSAnimationCancel:
281         case MediaTimelineRecord.TimestampType.CSSAnimationDone:
282             this._endTime = lastItem.timestamp;
283             break;
284
285         case MediaTimelineRecord.TimestampType.MediaElementDOMEvent:
286             if (WI.DOMNode.isPlayEvent(lastItem.eventName))
287                 this._endTime = NaN;
288             else if (!isNaN(this._endTime) || WI.DOMNode.isPauseEvent(lastItem.eventName) || WI.DOMNode.isStopEvent(lastItem.eventName))
289                 this._endTime = lastItem.timestamp;
290             break;
291         }
292
293         if (this.startTime !== oldStartTime || this.endTime !== oldEndTime)
294             this.dispatchEventToListeners(WI.TimelineRecord.Event.Updated);
295     }
296 };
297
298 WI.MediaTimelineRecord.EventType = {
299     CSSAnimation: "css-animation",
300     CSSTransition: "css-transition",
301     MediaElement: "media-element",
302 };
303
304 WI.MediaTimelineRecord.TimestampType = {
305     CSSAnimationReady: "css-animation-ready",
306     CSSAnimationDelay: "css-animation-delay",
307     CSSAnimationActive: "css-animation-active",
308     CSSAnimationCancel: "css-animation-cancel",
309     CSSAnimationDone: "css-animation-done",
310     // CSS transitions share the same timestamp types.
311
312     MediaElementDOMEvent: "media-element-dom-event",
313     MediaElementPowerEfficientPlaybackStateChange: "media-element-power-efficient-playback-state-change",
314 };