--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -362,50 +362,51 @@
// Set the current status icon.
if (this.hasError()) {
this.clickToPlay.hidden = true;
this.statusIcon.setAttribute("type", "error");
this.updateErrorText();
this.setupStatusFader(true);
}
- // An event handler for |onresize| should be added when bug 227495 is fixed.
- this.controlBar.hidden = false;
+ let adjustableControls = [
+ ...this.prioritizedControls,
+ this.controlBar,
+ this.clickToPlay
+ ];
- for (let control of this.layoutControls) {
+ for (let control of adjustableControls) {
if (!control) {
break;
}
Object.defineProperties(control, {
+ // We should directly access CSSOM to get pre-defined style instead of
+ // retrieving computed dimensions from layout.
minWidth: {
- value: control.clientWidth,
- writable: true
+ get: () => {
+ let controlAnonId = control.getAttribute("anonid");
+ let propertyName = `--${controlAnonId}-width`;
+ if (control.modifier) {
+ propertyName += "-" + control.modifier;
+ }
+ let preDefinedSize = this.controlBarComputedStyles.getPropertyValue(propertyName);
+
+ return parseInt(preDefinedSize, 10);
+ }
},
isAdjustableControl: {
value: true
},
- isWanted: {
- value: true,
- writable: true
- },
- resized: {
- value: false,
+ modifier: {
+ value: "",
writable: true
},
- resizedHandler: {
- value: () => {
- let width = control.clientWidth;
-
- if (width === 0) {
- return;
- }
-
- control.minWidth = width;
- },
+ isWanted: {
+ value: true,
writable: true
},
hideByAdjustment: {
set: (v) => {
if (v) {
control.setAttribute("hidden", "true");
} else {
control.removeAttribute("hidden");
@@ -416,34 +417,17 @@
get: () => control._isHiddenByAdjustment
},
_isHiddenByAdjustment: {
value: false,
writable: true
}
});
}
- // Cannot get minimal width of flexible scrubber and clickToPlay.
- // Rewrite to empirical value for now.
- this.scrubberStack.minWidth = 64;
- this.volumeStack.minWidth = 48;
- this.clickToPlay.minWidth = 48;
-
- if (this.positionDurationBox) {
- this.positionDurationBox.resizedHandler = () => {
- let durationWidth = this.durationSpan.hideByAdjustment ? 0 : this.durationSpan.clientWidth;
-
- this.positionDurationBox.minWidth = this.positionDurationBox.clientWidth - durationWidth;
- };
-
- this.positionDurationBox.resized = true;
- }
-
this.adjustControlSize();
- this.controlBar.hidden = true;
// Can only update the volume controls once we've computed
// _volumeControlWidth, since the volume slider implementation
// depends on it.
this.updateVolumeControls();
},
setupNewLoadState() {
@@ -589,20 +573,16 @@
if (this.video instanceof HTMLVideoElement &&
(this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
this.isAudioOnly = true;
this.clickToPlay.hidden = true;
this.startFadeIn(this.controlBar);
this.setFullscreenButtonState();
}
this.showPosition(Math.round(this.video.currentTime * 1000), Math.round(this.video.duration * 1000));
- if (this.positionDurationBox) {
- this.positionDurationBox.resized = true;
- this.durationSpan.resized = true;
- }
if (!this.isAudioOnly && !this.video.mozHasAudio) {
this.muteButton.setAttribute("noAudio", "true");
this.muteButton.setAttribute("disabled", "true");
}
this.adjustControlSize();
break;
case "loadeddata":
this.firstFrameShown = true;
@@ -833,16 +813,17 @@
const positionTextNode = Array.prototype.find.call(
this.positionDurationBox.childNodes, (n) => !!~n.textContent.search("#1"));
const durationSpan = this.durationSpan;
const durationFormat = durationSpan.textContent;
const positionFormat = positionTextNode.textContent;
durationSpan.classList.add("duration");
durationSpan.setAttribute("role", "none");
+ durationSpan.setAttribute("anonid", "durationSpan");
Object.defineProperties(this.positionDurationBox, {
durationSpan: {
value: durationSpan
},
position: {
set: (v) => {
positionTextNode.textContent = positionFormat.replace("#1", v);
@@ -859,31 +840,36 @@
showDuration(duration) {
let isInfinite = (duration == Infinity);
this.log("Duration is " + duration + "ms.\n");
if (isNaN(duration) || isInfinite) {
duration = this.maxCurrentTimeSeen;
}
+ // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+ this.showHours = (duration >= 3600000);
+
// Format the duration as "h:mm:ss" or "m:ss"
let timeString = isInfinite ? "" : this.formatTime(duration);
if (this.videocontrols.isTouchControls) {
this.durationLabel.setAttribute("value", timeString);
} else {
this.positionDurationBox.duration = timeString;
+
+ if (this.showHours) {
+ this.positionDurationBox.modifier = "long";
+ this.durationSpan.modifier = "long";
+ }
}
// "durationValue" property is used by scale binding to
// generate accessible name.
this.scrubber.durationValue = timeString;
- // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
- this.showHours = (duration >= 3600000);
-
this.scrubber.max = duration;
// XXX Can't set increment here, due to bug 473103. Also, doing so causes
// snapping when dragging with the mouse, so we can't just set a value for
// the arrow-keys.
this.scrubber.pageIncrement = Math.round(duration / 10);
},
pauseVideoDuringDragging() {
@@ -1172,36 +1158,16 @@
}
if (this.videocontrols.isTouchControls) {
this.scrubber.dragStateChanged(false);
}
element.hidden = true;
},
- onVideoControlsResized() {
- // Do not adjust again if resize event is caused by adjustControls(). Audio
- // might be resized by adjustControls(), so we should skip this handler if
- // current size is the same as the adjusted size.
- let {width, height} = this.video.getBoundingClientRect();
- if (width === this.adjustedVideoSize.width && height === this.adjustedVideoSize.height) {
- return;
- }
-
- // For the controls which haven't got correct computed size yet, force
- // them to re-cache their minWidth when the media is resized (reflow).
- this.layoutControls.forEach(control => {
- if (control) {
- control.resized = control.resized || (control.minWidth === 0);
- }
- });
-
- this.adjustControlSize();
- },
-
_triggeredByControls: false,
startPlay() {
this._triggeredByControls = true;
this.hideClickToPlay();
this.video.play();
},
@@ -1490,22 +1456,16 @@
return true;
}
}
return false;
},
setClosedCaptionButtonState() {
- this.adjustControlSize();
-
- if (!this.isClosedCaptionAvailable) {
- return;
- }
-
if (this.isClosedCaptionOn()) {
this.closedCaptionButton.setAttribute("enabled", "true");
} else {
this.closedCaptionButton.removeAttribute("enabled");
}
let ttItems = this.textTrackList.childNodes;
@@ -1513,16 +1473,18 @@
const idx = +tti.getAttribute("index");
if (idx == this.currentTextTrackIndex) {
tti.setAttribute("on", "true");
} else {
tti.removeAttribute("on");
}
}
+
+ this.adjustControlSize();
},
addNewTextTrack(tt) {
if (!this.isSupportedTextTrack(tt)) {
return;
}
if (tt.index && tt.index < this.textTracksCount) {
@@ -1661,119 +1623,99 @@
get isTopLevelSyntheticDocument() {
let doc = this.video.ownerDocument;
let win = doc.defaultView;
return doc.mozSyntheticDocument && win === win.top;
},
controlBarMinHeight: 40,
- adjustedVideoSize: {},
+ controlBarMinVisibleHeight: 28,
adjustControlSize() {
if (this.videocontrols.isTouchControls) {
return;
}
- let controlHidden = this.isControlBarHidden;
-
- if (this.layoutControls.some(control => control.resized)) {
- this.controlBar.hidden = false;
-
- for (let control of this.layoutControls) {
- if (control.resized && !control.hideByAdjustment) {
- control.resizedHandler();
- control.resized = false;
- }
- }
-
- this.controlBar.hidden = controlHidden;
- }
-
- // Check minWidth of controlBar before adjusting. If the layout information
- // isn't ready yet, the minWidth of controlBar would be undefined or 0, so early
- // return to avoid invalid adjustment.
- if (!this.controlBar.minWidth) {
- return;
- }
-
- let videoWidth = this.video.clientWidth;
- let videoHeight = this.video.clientHeight;
const minControlBarPaddingWidth = 18;
- // Hide and show control in order.
- const prioritizedControls = [
- this.playButton,
- this.muteButton,
- this.fullscreenButton,
- this.closedCaptionButton,
- this.positionDurationBox,
- this.scrubberStack,
- this.durationSpan,
- this.volumeStack
- ];
-
this.fullscreenButton.isWanted = !this.controlBar.hasAttribute("fullscreen-unavailable");
this.closedCaptionButton.isWanted = this.isClosedCaptionAvailable;
this.volumeStack.isWanted = !this.muteButton.hasAttribute("noAudio");
+ let minRequiredWidth = this.prioritizedControls
+ .filter(control => control && control.isWanted)
+ .reduce((accWidth, cc) => accWidth + cc.minWidth, minControlBarPaddingWidth);
+ // Skip the adjustment in case the stylesheets haven't been loaded yet.
+ if (!minRequiredWidth) {
+ return;
+ }
+
+ let givenHeight = this.video.clientHeight;
+ let videoWidth = this.video.clientWidth || minRequiredWidth;
+ let videoHeight = this.isAudioOnly ? this.controlBarMinHeight : givenHeight;
+
let widthUsed = minControlBarPaddingWidth;
let preventAppendControl = false;
- for (let control of prioritizedControls) {
+ for (let control of this.prioritizedControls) {
if (!control.isWanted) {
control.hideByAdjustment = true;
continue;
}
control.hideByAdjustment = preventAppendControl ||
widthUsed + control.minWidth > videoWidth;
if (control.hideByAdjustment) {
preventAppendControl = true;
} else {
widthUsed += control.minWidth;
}
}
- if (this.durationSpan.hideByAdjustment) {
- this.positionDurationBox.resized = true;
+ // Use flexible spacer to separate controls when scrubber is hidden.
+ // As long as muteButton hidden, which means only play button presents,
+ // hide spacer and make playButton centered.
+ this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
+
+ // Since the size of videocontrols is expanded with controlBar in <audio>, we
+ // should fix the dimensions in order not to recursively trigger reflow afterwards.
+ if (this.isAudioOnly) {
+ if (givenHeight) {
+ this.controlBar.style.height = `${Math.max(givenHeight, this.controlBarMinVisibleHeight)}px`;
+ }
+ this.controlBar.style.width = `${videoWidth - minControlBarPaddingWidth}px`;
+ return;
}
if (videoHeight < this.controlBarMinHeight ||
widthUsed === minControlBarPaddingWidth) {
this.controlBar.setAttribute("size", "hidden");
this.controlBar.hideByAdjustment = true;
} else {
this.controlBar.removeAttribute("size");
this.controlBar.hideByAdjustment = false;
}
- // Use flexible spacer to separate controls when scrubber is hidden.
- // As long as muteButton hidden, which means only play button presents,
- // hide spacer and make playButton centered.
- this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
-
// Adjust clickToPlayButton size.
const minVideoSideLength = Math.min(videoWidth, videoHeight);
const clickToPlayViewRatio = 0.15;
const clickToPlayScaledSize = Math.max(
this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
if (clickToPlayScaledSize >= videoWidth ||
(clickToPlayScaledSize + this.controlBarMinHeight / 2 >= videoHeight / 2 )) {
this.clickToPlay.hideByAdjustment = true;
} else {
if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
this.clickToPlay.hideByAdjustment = false;
}
this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
}
- // Record new size after adjustment
- this.adjustedVideoSize = this.video.getBoundingClientRect();
},
init(binding) {
this.video = binding.parentNode;
this.videocontrols = binding;
this.controlsContainer = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
this.statusIcon = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
@@ -1797,24 +1739,30 @@
this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
if (this.positionDurationBox) {
this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
}
- this.layoutControls = [
- ...this.controlBar.children,
+ this.controlBarComputedStyles = getComputedStyle(this.controlBar);
+
+ // Hide and show control in certain order.
+ this.prioritizedControls = [
+ this.playButton,
+ this.muteButton,
+ this.fullscreenButton,
+ this.closedCaptionButton,
+ this.positionDurationBox,
+ this.scrubberStack,
this.durationSpan,
- this.controlBar,
- this.clickToPlay
+ this.volumeStack
];
-
// XXX controlsContainer is a desktop only element. To determine whether
// isTouchControls or not during the whole initialization process, get
// this state overridden here.
this.videocontrols.isTouchControls = !this.controlsContainer;
this.isAudioOnly = (this.video instanceof HTMLAudioElement);
this.setupInitialState();
this.setupNewLoadState();
this.initTextTracks();
@@ -1846,17 +1794,17 @@
addListener(this.muteButton, "click", this.toggleMute);
addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
addListener(this.fullscreenButton, "click", this.toggleFullscreen);
addListener(this.playButton, "click", this.clickToPlayClickHandler);
addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
- addListener(this.videocontrols, "resizevideocontrols", this.onVideoControlsResized);
+ addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
addListener(this.video, "keypress", this.keyHandler, true);
addListener(this.videocontrols, "dragstart", function(event) {
event.preventDefault(); // prevent dragging of controls image (bug 517114)