blob: f3a075ae33bfd8ec118dbbd23f15fdda5f69c9ae [file] [log] [blame]
/*
* 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 WI.domManager.querySelector(documentNode, 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 WI.domManager.querySelector(documentNode, 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",
};