Skip to content

Commit 1c85396

Browse files
authored
feat(UI): Modernization of the UI (#8409)
Changes: - The look has been changed to make it more similar to YouTube: - The main background color is now black, and the font is white. - Presentation time has been moved to the bottom. - Cast and airplay buttons are now more accessible. - Tooltips have been enabled except on mobile platforms. - The ad information has been moved to appear in the same position as the presentation time when the ad is present. - A mark indicating the current quality has been added (e.g.: HD, 2K, 4K, 8K) - The spinner has been replaced with one that works well on Smart TVs and is very similar to the current one. The animation is included in the SVG element itself rather than through CSS. - More LESS variables have been added to make customization easier in forks. - The maximum size of the menus is dynamically calculated so that they never extend outside the video container. - The size of the subtitle container when the UI appears is now calculated dynamically. - The Demo has been updated to show the seekbar when trickplays are enabled. - UI performance on Smart TVs has been improved (Tested on Tizen 5.0) - Many offsets that were hardcoded have been removed, but not all (in CSS). Close #8406
1 parent a27434b commit 1c85396

33 files changed

+562
-561
lines changed

build/types/ui

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# UI library.
22

33
+../../third_party/language-mapping-list/language-mapping-list.js
4-
+../../ui/ad_counter.js
5-
+../../ui/ad_position.js
4+
+../../ui/ad_info.js
65
+../../ui/ad_statistics_button.js
76
+../../ui/audio_language_selection.js
87
+../../ui/externs/ui.js

demo/demo.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ main.mdl-layout__content {
7171
top: 10px;
7272
right: 10px;
7373

74+
height: 32px;
75+
width: 32px;
76+
font-size: 24px;
77+
7478
/* Give the button a round background, meant to look like the play button. */
7579
border-radius: 50%;
7680
color: #000;

demo/main.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,6 @@ shakaDemo.Main = class {
385385
return element != 'rewind' && element != 'fast_forward';
386386
});
387387
if (this.trickPlayControlsEnabled_) {
388-
// Trick mode controls don't have a seek bar.
389-
uiConfig.addSeekBar = false;
390388
// Replace the position the play_pause button was at with a full suite of
391389
// trick play controls, including rewind and fast-forward.
392390
const index = uiConfig.controlPanelElements.indexOf('play_pause');

lib/util/dom_utils.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ shaka.util.Dom = class {
105105
return shaka.util.Dom.asHTMLElement(elements[0]);
106106
}
107107

108+
109+
/**
110+
* Returns the element with a given class name if it exists.
111+
* Assumes the class name to be unique for a given parent.
112+
*
113+
* @param {string} className
114+
* @param {!HTMLElement} parent
115+
* @return {?HTMLElement}
116+
*/
117+
static getElementByClassNameIfItExists(className, parent) {
118+
const elements = parent.getElementsByClassName(className);
119+
if (!elements.length) {
120+
return null;
121+
}
122+
goog.asserts.assert(elements.length == 1,
123+
'Should only be one element with class name ' + className);
124+
125+
return shaka.util.Dom.asHTMLElement(elements[0]);
126+
}
127+
128+
108129
/**
109130
* Remove all of the child nodes of an element.
110131
* @param {!Element} element

shaka-player.uncompiled.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ goog.require('shaka.ui.PlayButton');
8383
goog.require('shaka.ui.SettingsMenu');
8484
goog.require('shaka.ui.OverflowMenu');
8585
goog.require('shaka.ui.AudioLanguageSelection');
86-
goog.require('shaka.ui.AdCounter');
87-
goog.require('shaka.ui.AdPosition');
86+
goog.require('shaka.ui.AdInfo');
8887
goog.require('shaka.ui.AdStatisticsButton');
8988
goog.require('shaka.ui.AirPlayButton');
9089
goog.require('shaka.ui.BigPlayButton');

test/text/text_displayer_layout_unit.js

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,50 +30,6 @@ filterDescribe('Cue layout', shaka.test.TextLayoutTests.supported, () => {
3030
});
3131

3232
defineTests();
33-
34-
// This test is unique to the UI.
35-
it('moves cues to avoid controls', async () => {
36-
let ui;
37-
38-
try {
39-
// Set up UI controls. The video element is in a paused state by
40-
// default, so the controls should be shown. The video is not in the
41-
// DOM and is purely temporary.
42-
const player = new shaka.Player();
43-
ui = new shaka.ui.Overlay(
44-
player, /** @type {!HTMLElement} */(helper.videoContainer),
45-
shaka.test.UiUtils.createVideoElement());
46-
// Turn off every part of the UI that we can, so that the screenshot is
47-
// less likely to change because of something unrelated to text
48-
// rendering.
49-
ui.configure({
50-
controlPanelElements: [],
51-
addSeekBar: false,
52-
addBigPlayButton: false,
53-
enableFullscreenOnRotation: false,
54-
});
55-
56-
// Recreate the text displayer so that the text container comes after
57-
// the controls (as it does in production). This is important for the
58-
// CSS that moves the cues above the controls when they are shown.
59-
await helper.textDisplayer.destroy();
60-
helper.recreateTextDisplayer();
61-
62-
const cue = new shaka.text.Cue(
63-
0, 1, 'Captain\'s log, stardate 41636.9');
64-
cue.region.id = '1';
65-
// Position the cue *explicitly* at the bottom of the screen.
66-
cue.region.viewportAnchorX = 0; // %
67-
cue.region.viewportAnchorY = 90; // %
68-
cue.region.width = 100; // %
69-
cue.region.height = 10; // %
70-
helper.textDisplayer.append([cue]);
71-
72-
await helper.checkScreenshot('cue-with-controls');
73-
} finally {
74-
await ui.destroy();
75-
}
76-
});
7733
});
7834

7935
describe('using browser-native rendering', () => {

test/ui/ad_ui_unit.js

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -208,82 +208,6 @@ describe('Ad UI', () => {
208208
});
209209
});
210210

211-
describe('ad counter', () => {
212-
/** @type {!HTMLElement} */
213-
let adCounter;
214-
/** @type {!shaka.ui.Localization} */
215-
let localization;
216-
217-
beforeEach(() => {
218-
adCounter = UiUtils.getElementByClassName(
219-
container, 'shaka-ad-counter-span');
220-
221-
localization = video['ui'].getControls().getLocalization();
222-
});
223-
224-
it('displays correct ad time', async () => {
225-
const eventManager = new shaka.util.EventManager();
226-
const waiter = new shaka.test.Waiter(eventManager);
227-
const p = waiter.waitForEvent(adManager, shaka.ads.Utils.AD_STARTED);
228-
229-
ad = new shaka.test.FakeAd(/* skipIn= */ null,
230-
/* position= */ 1, /* totalAdsInPod= */ 1);
231-
adManager.startAd(ad);
232-
ad.setRemainingTime(5); // seconds
233-
234-
await p;
235-
236-
// Ad counter has an internal timer that fires every 0.5 sec
237-
// to check the state. Give it a full second to make sure it
238-
// has time to catch up to the new remaining time value.
239-
await shaka.test.Util.delay(1);
240-
241-
const expectedTimeString = '0:05 / 0:10';
242-
const LocIds = shaka.ui.Locales.Ids;
243-
const raw = localization.resolve(LocIds.AD_TIME);
244-
expect(adCounter.textContent).toBe(
245-
raw.replace('[AD_TIME]', expectedTimeString));
246-
});
247-
});
248-
249-
describe('ad position', () => {
250-
/** @type {!HTMLElement} */
251-
let adPosition;
252-
/** @type {!shaka.ui.Localization} */
253-
let localization;
254-
255-
beforeEach(() => {
256-
adPosition = UiUtils.getElementByClassName(
257-
container, 'shaka-ad-position-span');
258-
259-
localization = video['ui'].getControls().getLocalization();
260-
});
261-
262-
263-
it('correctly shows "X of Y ads" for a sequence of several ads',
264-
async () => {
265-
const position = 2;
266-
const adsInPod = 3;
267-
const eventManager = new shaka.util.EventManager();
268-
const waiter = new shaka.test.Waiter(eventManager);
269-
const p = waiter.waitForEvent(
270-
adManager, shaka.ads.Utils.AD_STARTED);
271-
272-
ad = new shaka.test.FakeAd(/* skipIn= */ null,
273-
/* position= */ position, /* totalAdsInPod= */ adsInPod);
274-
adManager.startAd(ad);
275-
276-
await p;
277-
278-
const LocIds = shaka.ui.Locales.Ids;
279-
const expectedString = localization.resolve(LocIds.AD_PROGRESS)
280-
.replace('[AD_ON]', String(position))
281-
.replace('[NUM_ADS]', String(adsInPod));
282-
283-
expect(adPosition.textContent).toBe(expectedString);
284-
});
285-
});
286-
287211
describe('timeline', () => {
288212
/** @type {!HTMLElement} */
289213
let seekBar;

test/ui/ui_customization_unit.js

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ describe('UI Customization', () => {
5454
});
5555

5656
it('only the specified overflow menu buttons are created', async () => {
57-
const config = {overflowMenuButtons: ['cast']};
57+
const config = {overflowMenuButtons: ['loop']};
5858
await UiUtils.createUIThroughAPI(container, video, config, canvas);
5959

60-
UiUtils.confirmElementFound(container, 'shaka-cast-button');
60+
UiUtils.confirmElementFound(container, 'shaka-loop-button');
6161

6262
UiUtils.confirmElementMissing(container, 'shaka-caption-button');
6363
});
@@ -86,32 +86,11 @@ describe('UI Customization', () => {
8686
UiUtils.confirmElementFound(container, 'shaka-play-button');
8787
});
8888

89-
it('settings menus are lower when seek bar is absent', async () => {
90-
const config = {addSeekBar: false};
91-
await UiUtils.createUIThroughAPI(container, video, config, canvas);
92-
93-
function confirmLowPosition(className) {
94-
const elements =
95-
container.getElementsByClassName(className);
96-
expect(elements.length).toBe(1);
97-
expect(
98-
elements[0].classList.contains('shaka-low-position')).toBe(true);
99-
}
100-
101-
UiUtils.confirmElementMissing(container, 'shaka-seek-bar');
102-
103-
confirmLowPosition('shaka-overflow-menu');
104-
confirmLowPosition('shaka-resolutions');
105-
confirmLowPosition('shaka-audio-languages');
106-
confirmLowPosition('shaka-text-languages');
107-
confirmLowPosition('shaka-playback-rates');
108-
});
109-
11089
it('controls are created in specified order', async () => {
11190
const config = {
11291
controlPanelElements: [
11392
'mute',
114-
'time_and_duration',
93+
'loop',
11594
'fullscreen',
11695
],
11796
};
@@ -130,7 +109,7 @@ describe('UI Customization', () => {
130109
expect( /** @type {!HTMLElement} */ (buttons[0]).className)
131110
.toContain('shaka-mute-button');
132111
expect( /** @type {!HTMLElement} */ (buttons[1]).className)
133-
.toContain('shaka-current-time');
112+
.toContain('shaka-loop-button');
134113
expect( /** @type {!HTMLElement} */ (buttons[2]).className)
135114
.toContain('shaka-fullscreen');
136115
});

test/ui/ui_unit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ describe('UI', () => {
961961
const videos = container.getElementsByTagName('video');
962962
expect(videos.length).not.toBe(0);
963963

964-
UiUtils.confirmElementFound(container, 'shaka-spinner-svg');
964+
UiUtils.confirmElementFound(container, 'shaka-spinner');
965965
UiUtils.confirmElementFound(container, 'shaka-overflow-menu');
966966
UiUtils.confirmElementFound(container, 'shaka-controls-button-panel');
967967
}

ui/ad_counter.js renamed to ui/ad_info.js

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77

8-
goog.provide('shaka.ui.AdCounter');
8+
goog.provide('shaka.ui.AdInfo');
99

1010
goog.require('goog.asserts');
1111
goog.require('shaka.ads.Utils');
@@ -23,7 +23,7 @@ goog.requireType('shaka.ui.Controls');
2323
* @final
2424
* @export
2525
*/
26-
shaka.ui.AdCounter = class extends shaka.ui.Element {
26+
shaka.ui.AdInfo = class extends shaka.ui.Element {
2727
/**
2828
* @param {!HTMLElement} parent
2929
* @param {!shaka.ui.Controls} controls
@@ -32,14 +32,9 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
3232
super(parent, controls);
3333

3434
/** @private {!HTMLElement} */
35-
this.container_ = shaka.util.Dom.createHTMLElement('div');
36-
this.container_.classList.add('shaka-ad-counter');
37-
this.parent.appendChild(this.container_);
38-
39-
/** @private {!HTMLElement} */
40-
this.span_ = shaka.util.Dom.createHTMLElement('span');
41-
this.span_.classList.add('shaka-ad-counter-span');
42-
this.container_.appendChild(this.span_);
35+
this.adInfo_ = shaka.util.Dom.createButton();
36+
this.adInfo_.classList.add('shaka-ad-info');
37+
this.parent.appendChild(this.adInfo_);
4338

4439
/**
4540
* The timer that tracks down the ad progress.
@@ -97,6 +92,8 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
9792
* @private
9893
*/
9994
onTimerTick_() {
95+
const LocIds = shaka.ui.Locales.Ids;
96+
10097
goog.asserts.assert(this.ad != null,
10198
'this.ad should exist at this point');
10299

@@ -105,11 +102,23 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
105102
return;
106103
}
107104

105+
let text = '';
106+
107+
const adsInAdPod = this.ad.getSequenceLength();
108+
if (adsInAdPod > 1) {
109+
// If it's a single ad, showing 'Ad 1 of 1' isn't helpful.
110+
// Only show this element if there's more than 1 ad and it's a linear ad.
111+
const adPosition = this.ad.getPositionInSequence();
112+
text = this.localization.resolve(LocIds.AD_PROGRESS)
113+
.replace('[AD_ON]', String(adPosition))
114+
.replace('[NUM_ADS]', String(adsInAdPod));
115+
}
116+
108117
const secondsLeft = Math.round(this.ad.getRemainingTime());
109118
const adDuration = this.ad.getDuration();
110119
if (secondsLeft == -1 || adDuration == -1) {
111-
// Not enough information about the ad. Don't show the
112-
// counter just yet.
120+
this.adInfo_.textContent = text;
121+
shaka.ui.Utils.setDisplay(this.adInfo_, text != '');
113122
return;
114123
}
115124

@@ -121,17 +130,17 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
121130
adDuration, /* showHour= */ false);
122131
const timeString = timePassedStr + ' / ' + adLength;
123132

124-
const adsInAdPod = this.ad.getSequenceLength();
125133
// If there's more than one ad in the sequence, show the time
126134
// without the word 'Ad' (it will be shown by another element).
127135
// Otherwise, the format is "Ad: 0:05 / 0:10."
128136
if (adsInAdPod > 1) {
129-
this.span_.textContent = timeString;
137+
text += '\u00A0\u00A0' + timeString;
130138
} else {
131-
const LocIds = shaka.ui.Locales.Ids;
132-
const raw = this.localization.resolve(LocIds.AD_TIME);
133-
this.span_.textContent = raw.replace('[AD_TIME]', timeString);
139+
text = this.localization.resolve(LocIds.AD_TIME)
140+
.replace('[AD_TIME]', timeString);
134141
}
142+
this.adInfo_.textContent = text;
143+
shaka.ui.Utils.setDisplay(this.adInfo_, text != '');
135144
} else {
136145
this.reset_();
137146
}
@@ -144,7 +153,7 @@ shaka.ui.AdCounter = class extends shaka.ui.Element {
144153
this.timer_.stop();
145154
// Controls are going to hide the whole ad panel once the ad is over,
146155
// this is just a safeguard.
147-
this.span_.textContent = '';
156+
this.adInfo_.textContent = '';
148157
}
149158

150159
/**

0 commit comments

Comments
 (0)