Web Inspector: Timelines: add a timeline that shows information about any recorded...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / MediaTimelineDataGridNode.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.MediaTimelineDataGridNode = class MediaTimelineDataGridNode extends WI.TimelineDataGridNode
27 {
28     constructor(record, options = {})
29     {
30         console.assert(record instanceof WI.MediaTimelineRecord);
31
32         super([record], options);
33     }
34
35     // Public
36
37     get data()
38     {
39         if (this._cachedData)
40             return this._cachedData;
41
42         this._cachedData = super.data;
43         this._cachedData.name = this.record.displayName;
44         this._cachedData.element = this.record.domNode;
45         this._cachedData.source = this.record.domNode; // Timeline Overview
46         return this._cachedData;
47     }
48
49     createCellContent(columnIdentifier, cell)
50     {
51         let value = this.data[columnIdentifier];
52
53         switch (columnIdentifier) {
54         case "name":
55             cell.classList.add(...this.iconClassNames());
56             return this._createNameCellDocumentFragment();
57
58         case "element":
59         case "source": // Timeline Overview
60             if (!(value instanceof WI.DOMNode)) {
61                 cell.classList.add(WI.DOMTreeElementPathComponent.DOMNodeIconStyleClassName);
62                 return value.displayName;
63             }
64             break;
65         }
66
67         return super.createCellContent(columnIdentifier, cell);
68     }
69
70     // TimelineRecordBar delegate
71
72     timelineRecordBarCustomChildren(timelineRecordBar)
73     {
74         let children = [];
75
76         let record = this.record;
77         let timestamps = record.timestamps;
78
79         switch (record.eventType) {
80         case WI.MediaTimelineRecord.EventType.CSSAnimation:
81         case WI.MediaTimelineRecord.EventType.CSSTransition: {
82             let readyStartTime = NaN;
83             function addReadySegment(startTime, endTime) {
84                 children.push({
85                     startTime,
86                     endTime,
87                     classNames: ["segment", "css-animation-ready"],
88                     title: WI.UIString("Ready", "Tooltip for a time range bar that represents when a CSS animation/transition exists but has not started processing"),
89                 });
90                 readyStartTime = NaN;
91             }
92
93             let delayStartTime = NaN;
94             function addDelaySegment(startTime, endTime) {
95                 children.push({
96                     startTime,
97                     endTime,
98                     classNames: ["segment", "css-animation-delay"],
99                     title: WI.UIString("Delay", "Tooltip for a time range bar that represents when a CSS animation/transition is delayed"),
100                 });
101                 delayStartTime = NaN;
102             }
103
104             let activeStartTime = NaN;
105             function addActiveSegment(startTime, endTime) {
106                 children.push({
107                     startTime,
108                     endTime,
109                     classNames: ["segment", "css-animation-active"],
110                     title: WI.UIString("Active", "Tooltip for a time range bar that represents when a CSS animation/transition is running"),
111                 });
112                 activeStartTime = NaN;
113             }
114
115             for (let item of timestamps) {
116                 switch (item.type) {
117                 case WI.MediaTimelineRecord.TimestampType.CSSAnimationReady:
118                     if (isNaN(readyStartTime))
119                         readyStartTime = item.timestamp;
120                     break;
121                 case WI.MediaTimelineRecord.TimestampType.CSSAnimationDelay:
122                     if (isNaN(delayStartTime))
123                         delayStartTime = item.timestamp;
124                     if (!isNaN(readyStartTime))
125                         addReadySegment(readyStartTime, item.timestamp);
126                     break;
127                 case WI.MediaTimelineRecord.TimestampType.CSSAnimationActive:
128                     if (isNaN(activeStartTime))
129                         activeStartTime = item.timestamp;
130                     if (!isNaN(readyStartTime))
131                         addReadySegment(readyStartTime, item.timestamp);
132                     if (!isNaN(delayStartTime))
133                         addDelaySegment(delayStartTime, item.timestamp);
134                     break;
135                 case WI.MediaTimelineRecord.TimestampType.CSSAnimationCancel:
136                 case WI.MediaTimelineRecord.TimestampType.CSSAnimationDone:
137                     if (!isNaN(readyStartTime))
138                         addReadySegment(readyStartTime, item.timestamp);
139                     if (!isNaN(delayStartTime))
140                         addDelaySegment(delayStartTime, item.timestamp);
141                     if (!isNaN(activeStartTime))
142                         addActiveSegment(activeStartTime, item.timestamp);
143                     break;
144                 }
145             }
146
147             if (!isNaN(readyStartTime))
148                 addReadySegment(readyStartTime, NaN);
149             if (!isNaN(delayStartTime))
150                 addDelaySegment(delayStartTime, NaN);
151             if (!isNaN(activeStartTime))
152                 addActiveSegment(activeStartTime, NaN);
153
154             break;
155         }
156
157         case WI.MediaTimelineRecord.EventType.MediaElement: {
158             let fullScreenSegments = [];
159             let powerEfficientPlaybackSegments = [];
160             let activeSegments = [];
161
162             let fullScreenStartTime = NaN;
163             let fullScreenOriginator = null;
164             function addFullScreenSegment(startTime, endTime) {
165                 fullScreenSegments.push({
166                     startTime,
167                     endTime,
168                     classNames: ["segment", "media-element-full-screen"],
169                     title: fullScreenOriginator ? WI.UIString("Full-Screen from \u201C%s\u201D").format(fullScreenOriginator.displayName) : WI.UIString("Full-Screen"),
170                 });
171                 fullScreenStartTime = NaN;
172                 fullScreenOriginator = null;
173             }
174
175             let powerEfficientPlaybackStartTime = NaN;
176             function addPowerEfficientPlaybackSegment(startTime, endTime) {
177                 powerEfficientPlaybackSegments.push({
178                     startTime,
179                     endTime,
180                     classNames: ["segment", "media-element-power-efficient-playback"],
181                     title: WI.UIString("Power Efficient Playback"),
182                 });
183                 powerEfficientPlaybackStartTime = NaN;
184             }
185
186             let pausedStartTime = NaN;
187             function addPausedSegment(startTime, endTime) {
188                 activeSegments.push({
189                     startTime,
190                     endTime,
191                     classNames: ["segment", "media-element-paused"],
192                     title: WI.UIString("Paused", "Tooltip for a time range bar that represents when the playback of a audio/video element is paused"),
193                 });
194                 pausedStartTime = NaN;
195             }
196
197             let playingStartTime = NaN;
198             function addPlayingSegment(startTime, endTime) {
199                 activeSegments.push({
200                     startTime,
201                     endTime,
202                     classNames: ["segment", "media-element-playing"],
203                     title: WI.UIString("Playing", "Tooltip for a time range bar that represents when the playback of a audio/video element is running"),
204                 });
205                 playingStartTime = NaN;
206             }
207
208             for (let item of timestamps) {
209                 if (item.type === WI.MediaTimelineRecord.TimestampType.MediaElementDOMEvent) {
210                     if (WI.DOMNode.isPlayEvent(item.eventName)) {
211                         if (isNaN(playingStartTime))
212                             playingStartTime = item.timestamp;
213                         if (!isNaN(pausedStartTime))
214                             addPausedSegment(pausedStartTime, item.timestamp);
215                     } else if (WI.DOMNode.isPauseEvent(item.eventName)) {
216                         if (isNaN(pausedStartTime))
217                             pausedStartTime = item.timestamp;
218                         if (!isNaN(playingStartTime))
219                             addPlayingSegment(playingStartTime, item.timestamp);
220                     } else if (WI.DOMNode.isStopEvent(item.eventName)) {
221                         if (!isNaN(pausedStartTime))
222                             addPausedSegment(pausedStartTime, item.timestamp);
223                         if (!isNaN(playingStartTime))
224                             addPlayingSegment(playingStartTime, item.timestamp);
225                     } else if (item.eventName === "webkitfullscreenchange") {
226                         if (!fullScreenOriginator && item.originator)
227                             fullScreenOriginator = item.originator;
228
229                         if (isNaN(fullScreenStartTime)) {
230                             if (item.data && item.data.enabled)
231                                 fullScreenStartTime = item.timestamp;
232                             else
233                                 addFullScreenSegment(this.graphDataSource ? this.graphDataSource.startTime : record.startTime, item.timestamp);
234                         } else if (!item.data || !item.data.enabled)
235                             addFullScreenSegment(fullScreenStartTime, item.timestamp);
236                     }
237                 } else if (item.type === WI.MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange) {
238                     if (isNaN(powerEfficientPlaybackStartTime)) {
239                         if (item.isPowerEfficient)
240                             powerEfficientPlaybackStartTime = item.timestamp;
241                         else
242                             addPowerEfficientPlaybackSegment(this.graphDataSource ? this.graphDataSource.startTime : record.startTime, item.timestamp);
243                     } else if (!item.isPowerEfficient)
244                         addPowerEfficientPlaybackSegment(powerEfficientPlaybackStartTime, item.timestamp);
245                 }
246             }
247
248             if (!isNaN(fullScreenStartTime))
249                 addFullScreenSegment(fullScreenStartTime, NaN);
250             if (!isNaN(powerEfficientPlaybackStartTime))
251                 addPowerEfficientPlaybackSegment(powerEfficientPlaybackStartTime, NaN);
252             if (!isNaN(pausedStartTime))
253                 addPausedSegment(pausedStartTime, NaN);
254             if (!isNaN(playingStartTime))
255                 addPlayingSegment(playingStartTime, NaN);
256
257             children.pushAll(fullScreenSegments);
258             children.pushAll(powerEfficientPlaybackSegments);
259             children.pushAll(activeSegments);
260             break;
261         }
262         }
263
264         timestamps.forEach((item, i) => {
265             let image = {
266                 startTime: item.timestamp,
267                 classNames: [],
268             };
269
270             switch (item.type) {
271             case WI.MediaTimelineRecord.TimestampType.CSSAnimationReady:
272             case WI.MediaTimelineRecord.TimestampType.CSSAnimationDelay:
273             case WI.MediaTimelineRecord.TimestampType.CSSAnimationDone:
274             case WI.MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange:
275                 // These timestamps are handled by the range segments above.
276                 return;
277
278             case WI.MediaTimelineRecord.TimestampType.CSSAnimationActive:
279                 // Don't create a marker segment for the first active timestamp, as that will be
280                 // handled by an active range segment above.
281                 if (!i || timestamps[i - 1].type !== WI.MediaTimelineRecord.TimestampType.CSSAnimationActive)
282                     return;
283
284                 image.image = "Images/EventIteration.svg";
285                 image.title = WI.UIString("Iteration", "Tooltip for a timestamp marker that represents when a CSS animation/transition iterates");
286                 break;
287
288             case WI.MediaTimelineRecord.TimestampType.CSSAnimationCancel:
289                 image.image = "Images/EventCancel.svg";
290                 image.title = WI.UIString("Canceled", "Tooltip for a timestamp marker that represents when a CSS animation/transition is canceled");
291                 break;
292
293             case WI.MediaTimelineRecord.TimestampType.MediaElementDOMEvent:
294                 // Don't create a marker segment full-screen timestamps, as that will be handled by a
295                 // range segment above.
296                 if (item.eventName === "webkitfullscreenchange")
297                     return;
298
299                 image.title = WI.UIString("DOM Event \u201C%s\u201D").format(item.eventName);
300                 if (WI.DOMNode.isPlayEvent(item.eventName))
301                     image.image = "Images/EventPlay.svg";
302                 else if (WI.DOMNode.isPauseEvent(item.eventName))
303                     image.image = "Images/EventPause.svg";
304                 else if (WI.DOMNode.isStopEvent(item.eventName))
305                     image.image = "Images/EventStop.svg";
306                 else
307                     image.image = "Images/EventProcessing.svg";
308                 break;
309             }
310
311             children.push(image);
312         });
313
314         return children;
315     }
316
317     // Protected
318
319     filterableDataForColumn(columnIdentifier)
320     {
321         switch (columnIdentifier) {
322         case "name":
323             return [this.record.displayName, this.record.subtitle];
324
325         case "element":
326         case "source": // Timeline Overview
327             if (this.record.domNode)
328                 return this.record.domNode.displayName;
329         }
330
331         return super.filterableDataForColumn(columnIdentifier);
332     }
333
334     // Private
335
336     _createNameCellDocumentFragment()
337     {
338         let fragment = document.createDocumentFragment();
339         fragment.append(this.record.displayName);
340
341         if (this.record.subtitle) {
342             let subtitleElement = fragment.appendChild(document.createElement("span"));
343             subtitleElement.className = "subtitle";
344             subtitleElement.textContent = this.record.subtitle;
345         }
346
347         return fragment;
348     }
349 };