Bug 1344609 - Pseudo classes support. r=rillian draft
authorbechen <bechen@mozilla.com>
Thu, 09 Mar 2017 16:03:39 +0800
changeset 495738 ef41f90a9b5b7525bc6b0397b9184c28d7d673c0
parent 495737 9f246e65abf8680294da55ed2e2fa74100c43847
child 548459 80fab416d8767c67195d5e81f5d1c3e66e983056
push id48423
push userbechen@mozilla.com
push dateThu, 09 Mar 2017 08:04:58 +0000
reviewersrillian
bugs1344609
milestone55.0a1
Bug 1344609 - Pseudo classes support. r=rillian MozReview-Commit-ID: 6ygjXvlbyJW
dom/html/TextTrackManager.cpp
dom/html/TextTrackManager.h
dom/media/webvtt/WebVTTParserWrapper.js
dom/media/webvtt/nsIWebVTTParserWrapper.idl
dom/media/webvtt/vtt.jsm
--- a/dom/html/TextTrackManager.cpp
+++ b/dom/html/TextTrackManager.cpp
@@ -293,16 +293,42 @@ TextTrackManager::UpdateCueDisplay()
     }
   } else if (overlay->Length() > 0) {
     WEBVTT_LOG("UpdateCueDisplay EmptyString");
     nsContentUtils::SetNodeTextContent(overlay, EmptyString(), true);
   }
 }
 
 void
+TextTrackManager::ApplyPseudoClasses()
+{
+  WEBVTT_LOG("ApplyPseudoClasses");
+
+  if (!mMediaElement || !mTextTracks) {
+    return;
+  }
+
+  nsIFrame* frame = mMediaElement->GetPrimaryFrame();
+  nsVideoFrame* videoFrame = do_QueryFrame(frame);
+  if (!videoFrame) {
+    return;
+  }
+
+  nsCOMPtr<nsIContent> overlay = videoFrame->GetCaptionOverlay();
+  if (!overlay) {
+    return;
+  }
+  nsPIDOMWindowInner* window = mMediaElement->OwnerDoc()->GetInnerWindow();
+  if (window) {
+    sParserWrapper->ApplyPseudoClasses(window, overlay,
+                                       mMediaElement->CurrentTime());
+  }
+}
+
+void
 TextTrackManager::NotifyCueAdded(TextTrackCue& aCue)
 {
   WEBVTT_LOG("NotifyCueAdded");
   if (mNewCues) {
     mNewCues->AddCue(aCue);
   }
   DispatchTimeMarchesOn();
   ReportTelemetryForCue();
@@ -702,16 +728,17 @@ TextTrackManager::TimeMarchesOn()
   for (uint32_t i = 0; i < otherCues->Length(); ++i) {
     if ((*otherCues)[i]->GetActive()) {
       c2 = false;
       break;
     }
   }
   bool c3 = (missedCues->Length() == 0);
   if (c1 && c2 && c3) {
+    ApplyPseudoClasses();
     mLastTimeMarchesOnCalled = currentPlaybackTime;
     WEBVTT_LOG("TimeMarchesOn step 7 return, mLastTimeMarchesOnCalled %lf", mLastTimeMarchesOnCalled);
     return;
   }
 
   // Step 8. Respect PauseOnExit flag if not seek.
   if (hasNormalPlayback) {
     for (uint32_t i = 0; i < otherCues->Length(); ++i) {
@@ -803,16 +830,17 @@ TextTrackManager::TimeMarchesOn()
     }
   }
 
   mLastTimeMarchesOnCalled = currentPlaybackTime;
   mLastActiveCues = currentCues;
 
   // Step 18.
   UpdateCueDisplay();
+  ApplyPseudoClasses();
 }
 
 void
 TextTrackManager::NotifyCueUpdated(TextTrackCue *aCue)
 {
   // TODO: Add/Reorder the cue to mNewCues if we have some optimization?
   WEBVTT_LOG("NotifyCueUpdated");
   DispatchTimeMarchesOn();
--- a/dom/html/TextTrackManager.h
+++ b/dom/html/TextTrackManager.h
@@ -108,16 +108,18 @@ public:
 private:
   /**
    * Converts the TextTrackCue's cuetext into a tree of DOM objects
    * and attaches it to a div on its owning TrackElement's
    * MediaElement's caption overlay.
    */
   void UpdateCueDisplay();
 
+  void ApplyPseudoClasses();
+
   // List of the TextTrackManager's owning HTMLMediaElement's TextTracks.
   RefPtr<TextTrackList> mTextTracks;
   // List of text track objects awaiting loading.
   RefPtr<TextTrackList> mPendingTextTracks;
   // List of newly introduced Text Track cues.
 
   // Contain all cues for a MediaElement. Not sorted.
   RefPtr<TextTrackCueList> mNewCues;
--- a/dom/media/webvtt/WebVTTParserWrapper.js
+++ b/dom/media/webvtt/WebVTTParserWrapper.js
@@ -54,16 +54,21 @@ WebVTTParserWrapper.prototype =
     return WebVTT.convertCueToDOMTree(window, cue.text);
   },
 
   processCues: function(window, cues, overlay, controls)
   {
     WebVTT.processCues(window, cues, overlay, controls);
   },
 
+  applyPseudoClasses: function(window, overlay, timestamp)
+  {
+    WebVTT.applyPseudoClasses(window, overlay, timestamp);
+  },
+
   classDescription: "Wrapper for the JS WebVTT implementation (vtt.js)",
   classID: Components.ID(WEBVTTPARSERWRAPPER_CID),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebVTTParserWrapper]),
   classInfo: XPCOMUtils.generateCI({
     classID:    WEBVTTPARSERWRAPPER_CID,
     contractID: WEBVTTPARSERWRAPPER_CONTRACTID,
     interfaces: [Ci.nsIWebVTTParserWrapper]
   })
--- a/dom/media/webvtt/nsIWebVTTParserWrapper.idl
+++ b/dom/media/webvtt/nsIWebVTTParserWrapper.idl
@@ -76,13 +76,24 @@ interface nsIWebVTTParserWrapper : nsISu
    *                and containing div element.
    * @param cues    An array of VTTCues who need there display state to be
    *                computed.
    * @param overlay The HTMLElement that the cues will be displayed within.
    * @param controls The video control element that will affect cues position.
    */
   void processCues(in mozIDOMWindow window, in nsIVariant cues,
                    in nsISupports overlay, in nsISupports controls);
+
+  /**
+   * Apply pseudo classes :past and :future to the overlay.
+   * @param window  A window object with which it will create the DOM tree
+   *                and containing div element.
+   * @param overlay The root of the cues will be displayed within.
+   * @param timestamp The video element playback time.
+   */
+  void applyPseudoClasses(in mozIDOMWindow window,
+                          in nsISupports overlay,
+                          in double timestamp);
 };
 
 %{C++
 #define NS_WEBVTTPARSERWRAPPER_CONTRACTID "@mozilla.org/webvttParserWrapper;1"
 %}
--- a/dom/media/webvtt/vtt.jsm
+++ b/dom/media/webvtt/vtt.jsm
@@ -23,16 +23,19 @@ this.EXPORTED_SYMBOLS = ["WebVTT"];
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 var Cu = Components.utils;
 Cu.import('resource://gre/modules/Services.jsm');
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
+const nodeConstants = require("devtools/shared/dom-node-constants");
 
 (function(global) {
 
   var _objCreate = Object.create || (function() {
     function F() {}
     return function(o) {
       if (arguments.length !== 1) {
         throw new Error('Object.create shim only accepts one parameter.');
@@ -399,16 +402,18 @@ Cu.import('resource://gre/modules/Servic
               tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
             tagStack.pop();
             current = current.parentNode;
           }
           // Otherwise just ignore the end tag.
           continue;
         }
         var ts = collectTimeStamp(t.substr(1, t.length - 2));
+        // TODO: verify the ts is valid or not, compare to the cue.startTime endTime?
+        // TODO: ensure the all ts are monotonic inscrease?
         var node;
         if (ts) {
           // Timestamps are lead nodes as well.
           node = window.document.createProcessingInstruction("timestamp", normalizedTimeStamp(ts));
           current.appendChild(node);
           continue;
         }
         var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
@@ -957,30 +962,108 @@ Cu.import('resource://gre/modules/Servic
       }
 
       for (var i = 0; i < cues.length; i++) {
         cue = cues[i];
 
         // Compute the intial position and styles of the cue div.
         styleBox = new CueStyleBox(window, cue, styleOptions);
         styleBox.cueDiv.style.setProperty("--cue-font-size", fontSize + "px ");
+        styleBox.cueDiv.lastTimestamp = undefined;
         paddedOverlay.appendChild(styleBox.div);
 
         // Move the cue div to it's correct line position.
         moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
 
         // Remember the computed div so that we don't have to recompute it later
         // if we don't have too.
         cue.displayState = styleBox.div;
 
         boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
       }
     })();
   };
 
+  WebVTT.applyPseudoClasses = function(window, overlay, playbackTime) {
+    var paddedOverlay = overlay.firstChild;
+    if (!paddedOverlay)
+      return;
+    // The cueRoot map to the styleBox.div, and the cueDiv map to styleBox.cueDiv
+    for (var cueRoot of paddedOverlay.childNodes) {
+      var cueDiv = cueRoot.firstChild;
+      if (!cueDiv) {
+        continue;
+      }
+
+      var timestampWalker = window.document.createTreeWalker(cueDiv,
+        nodeFilterConstants.SHOW_PROCESSING_INSTRUCTION, null, false);
+      var targetNode;
+      var lastNode = timestampWalker.nextNode();
+      // The cue doesn't have timestamp node inside.
+      if (!lastNode) {
+        // TODO: Do we still need to apply :past or :future?
+        continue;
+      }
+
+      // Compare playbackTime to the find the timestamp node.
+      if (playbackTime < collectTimeStamp(lastNode.data)) {
+        targetNode = lastNode;
+      } else {
+        targetNode = null;
+        while (timestampWalker.nextNode()) {
+          var node = timestampWalker.currentNode;
+          if (collectTimeStamp(node.data) > playbackTime) {
+            targetNode = node;
+            break;
+          }
+          lastNode = node;
+        }
+      }
+
+      // The cue had already applied pseudo-classes.
+      if (targetNode && targetNode.data === cueDiv.lastTimestamp) {
+        continue;
+      } else if (targetNode == null && cueDiv.lastTimestamp == null) {
+        continue;
+      }
+      // Remember the targetNode.data
+      if (targetNode) {
+        cueDiv.lastTimestamp = targetNode.data;
+      } else {
+        cueDiv.lastTimestamp = null;
+      }
+
+      // targetNode is null means playbackTime are greater than all timestamp nodes.
+      // We should apply all nodes to :past
+      var textWalker = window.document.createTreeWalker(cueDiv,
+        nodeFilterConstants.SHOW_TEXT | nodeFilterConstants.SHOW_PROCESSING_INSTRUCTION,
+        null, false);
+      // apply :past
+      while (textWalker.nextNode()) {
+        var node = textWalker.currentNode;
+        if (node.nodeType == nodeConstants.TEXT_NODE) {
+          // TODO: apply :past
+          dump("apply :past\n");
+          dump(node.data + "\n");
+        } else if (node === targetNode){
+          break;
+        }
+      }
+      // apply :future
+      while(textWalker.nextNode()) {
+        var node = textWalker.currentNode;
+        if (node.nodeType == nodeConstants.TEXT_NODE) {
+          // TODO: apply :future
+          dump("apply :future\n");
+          dump(node.data + "\n");
+        }
+      }
+    }
+  };
+
   WebVTT.Parser = function(window, decoder) {
     this.window = window;
     this.state = "INITIAL";
     this.buffer = "";
     this.decoder = decoder || new TextDecoder("utf8");
     this.regionList = [];
   };