shaka-player 3.3.16 → 3.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.github/workflows/build-and-test.yaml +4 -4
  2. package/.github/workflows/demo-version-index.yaml +6 -0
  3. package/.github/workflows/selenium-lab-tests.yaml +72 -11
  4. package/.github/workflows/update-screenshots.yaml +142 -0
  5. package/CHANGELOG.md +38 -0
  6. package/demo/common/asset.js +15 -0
  7. package/demo/common/assets.js +4 -2
  8. package/demo/common/message_ids.js +1 -0
  9. package/demo/custom.js +19 -0
  10. package/demo/locales/en.json +1 -0
  11. package/demo/locales/source.json +4 -0
  12. package/demo/main.js +14 -0
  13. package/dist/demo.compiled.debug.js +146 -144
  14. package/dist/demo.compiled.debug.map +3 -3
  15. package/dist/demo.compiled.js +146 -144
  16. package/dist/demo.compiled.map +3 -3
  17. package/dist/deps.js +1 -1
  18. package/dist/locales.js +28 -28
  19. package/dist/receiver.compiled.debug.js +15 -15
  20. package/dist/receiver.compiled.debug.map +2 -2
  21. package/dist/receiver.compiled.js +15 -15
  22. package/dist/receiver.compiled.map +2 -2
  23. package/dist/shaka-player.compiled.d.ts +12 -0
  24. package/dist/shaka-player.compiled.debug.d.ts +12 -0
  25. package/dist/shaka-player.compiled.debug.externs.js +4 -0
  26. package/dist/shaka-player.compiled.debug.js +105 -100
  27. package/dist/shaka-player.compiled.debug.map +3 -3
  28. package/dist/shaka-player.compiled.externs.js +4 -0
  29. package/dist/shaka-player.compiled.js +296 -292
  30. package/dist/shaka-player.compiled.map +3 -3
  31. package/dist/shaka-player.ui.d.ts +12 -0
  32. package/dist/shaka-player.ui.debug.d.ts +12 -0
  33. package/dist/shaka-player.ui.debug.externs.js +4 -0
  34. package/dist/shaka-player.ui.debug.js +119 -114
  35. package/dist/shaka-player.ui.debug.map +3 -3
  36. package/dist/shaka-player.ui.externs.js +4 -0
  37. package/dist/shaka-player.ui.js +311 -307
  38. package/dist/shaka-player.ui.map +3 -3
  39. package/docs/tutorials/errors.md +94 -0
  40. package/docs/tutorials/index.json +2 -0
  41. package/docs/tutorials/widevine-service-certs.md +45 -0
  42. package/externs/ima.js +9 -0
  43. package/externs/shaka/text.js +7 -0
  44. package/lib/ads/client_side_ad_manager.js +6 -3
  45. package/lib/dash/segment_template.js +17 -5
  46. package/lib/hls/hls_parser.js +3 -1
  47. package/lib/media/transmuxer.js +13 -7
  48. package/lib/media/video_wrapper.js +3 -3
  49. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  50. package/lib/player.js +3 -1
  51. package/lib/polyfill/media_capabilities.js +3 -8
  52. package/lib/polyfill/mediasource.js +7 -2
  53. package/lib/text/cue.js +6 -0
  54. package/lib/text/ui_text_displayer.js +2 -5
  55. package/lib/text/vtt_text_parser.js +257 -93
  56. package/lib/util/mime_utils.js +1 -1
  57. package/lib/util/platform.js +8 -0
  58. package/lib/util/stream_utils.js +26 -14
  59. package/package.json +1 -1
  60. package/support.html +1 -0
  61. package/test/dash/dash_parser_manifest_unit.js +0 -36
  62. package/test/polyfill/media_capabilities_unit.js +20 -22
  63. package/test/test/util/layout_tests.js +433 -0
  64. package/test/test/util/util.js +0 -120
  65. package/test/{ui → text}/text_displayer_layout_unit.js +59 -235
  66. package/test/text/ui_text_displayer_unit.js +2 -4
  67. package/test/text/vtt_text_parser_unit.js +151 -6
  68. package/test/text/web_vtt_layout_integration.js +525 -0
@@ -0,0 +1,94 @@
1
+ # Error Handling
2
+
3
+ The basics for handling native Error and {@link shaka.util.Error} thrown by Shaka.
4
+
5
+ ## Listening To and Handling Errors
6
+
7
+ ```javascript
8
+ const handleError = (error) => {
9
+ if (error instanceof Error) {
10
+ // shaka crashed with an unhandled native error
11
+ }
12
+
13
+ if (error.severity === shaka.util.Error.Severity.CRITICAL) {
14
+ // handle fatal error, playback can not continue
15
+ } else {
16
+ // handle non-fatal error, playback can continue
17
+ }
18
+ };
19
+
20
+ const player = new shaka.Player(video);
21
+
22
+ // handle errors that occur after load
23
+ player.addEventListener('error', handleError);
24
+
25
+ // there are two options for catching errors that occur during load
26
+
27
+ // it's possible to listen directly on the promise
28
+ player.load(url).catch(handleError);
29
+
30
+ // or to use async/await with a try/catch
31
+ try {
32
+ await player.load(url);
33
+ } catch (e) {
34
+ handleError(e);
35
+ }
36
+ ```
37
+
38
+ ## Custom Handling of Streaming Errors
39
+
40
+ The `streaming.failureCallback` property of {@link shaka.extern.PlayerConfiguration} can be used to add custom handling or error conversion of errors that occur during streaming.
41
+
42
+ ```javascript
43
+ player.configure('streaming.failureCallback', (error) => {
44
+ if (error.severity === shaka.util.Error.Severity.CRITICAL) {
45
+ // custom handling of critical error
46
+ // e.g. player.retryStreaming();
47
+ } else {
48
+ // custom handling of recoverable error
49
+ }
50
+ });
51
+ ```
52
+
53
+ ## Custom Handling of Retries
54
+
55
+ When configuring retry parameters in Shaka there may be known error codes that should not be retried, or the need to break out of an infinite retry loop in a live context. Retries for failed network requests are not reported as `RECOVERABLE` errors.
56
+
57
+ For example, if a VOD manifest is missing, unlike when a LIVE manifest is missing, it can be expected to not show up and there is no need to retry.
58
+
59
+ This is how to convert a retry into a critical error, see {@link shaka.net.NetworkingEngine.RetryEvent} and {@link shaka.util.Error} to decipher how the event can be parsed:
60
+
61
+ ```javascript
62
+ const nwEngine = player.getNetworkingEngine();
63
+
64
+ const vodManifestNotFoundHandler = (event /* shaka.net.NetworkingEngine.RetryEvent */) => {
65
+ const code = event.error.code;
66
+ const data = event.error.data;
67
+
68
+ if (code === shaka.util.Error.Code.BAD_HTTP_STATUS) {
69
+ if (
70
+ // each type of error has its own data structure (or none at all), tread with care
71
+ Array.isArray(data) &&
72
+ data[1] === 404 &&
73
+ data[4] === shaka.net.NetworkingEngine.RequestType.MANIFEST
74
+ ) {
75
+ // Throwing inside a retry callback will immediately stop retries
76
+ throw error;
77
+
78
+ // A proprietary error code can also be thrown
79
+ // throw new shaka.util.Error(
80
+ // shaka.util.Error.Severity.CRITICAL,
81
+ // shaka.util.Error.Category.NETWORK,
82
+ // 'RECOGNIZABLE_ERROR_MESSAGE'
83
+ // );
84
+ }
85
+ }
86
+ };
87
+
88
+ nwEngine.addEventListener('retry', vodManifestNotFoundHandler);
89
+
90
+ player.addEventListener('load'() => {
91
+ nwEngine.removeEventListener('retry', vodManifestNotFoundHandler);
92
+ });
93
+
94
+ ```
@@ -9,6 +9,7 @@
9
9
  { "license-wrapping": { "title": "License Wrapping" } },
10
10
  { "ui": { "title": "UI Library" } },
11
11
  { "ui-customization": { "title": "Configuring the UI" } },
12
+ { "errors": { "title": "Error Handling" } },
12
13
  { "a11y": { "title": "Creating accessible buttons" } },
13
14
  { "ad_monetization": { "title": "Serving ads with our IMA SDK integration" } },
14
15
  { "plugins": { "title": "Plugins and Customizing the Build" } },
@@ -16,6 +17,7 @@
16
17
  { "architecture": { "title": "Architecture Diagrams" } },
17
18
  { "service-worker": { "title": "Service Worker Caching" } },
18
19
  { "offline": { "title": "Offline Storage and Playback" } },
20
+ { "widevine-service-certs": { "title": "Widevine Service Certificates" } },
19
21
  { "fairplay": { "title": "FairPlay support" } },
20
22
  { "application-level-redirects": { "title": "Application-Level Redirects" } },
21
23
  { "blob-url": { "title": "Blob URL" } },
@@ -0,0 +1,45 @@
1
+ # Widevine Service Certificates
2
+
3
+ With Widevine, you can eliminate one round-trip to your license server or proxy
4
+ per session by preloading a Widevine service certificate. You can either provide
5
+ it directly or set a URI and let Shaka fetch it for you. Do this before calling
6
+ `player.load()`. The values will persist across multiple calls to `load()` on
7
+ the same `shaka.Player` instance.
8
+
9
+ ```js
10
+ // This is an example of loading the certificate from your site at runtime.
11
+ // You could also choose to bundle it into your JavaScript as a Uint8Array.
12
+ const req = await fetch('https://example.com/service.cert');
13
+ const cert = new Uint8Array(await req.arrayBuffer());
14
+
15
+ // This is the short form for configuration of a certificate:
16
+ player.configure('drm.advanced.com\\.widevine\\.alpha.serverCertificate',
17
+ cert);
18
+
19
+ // This is the long form:
20
+ player.configure({
21
+ drm: {
22
+ advanced: {
23
+ 'com.widevine.alpha': {
24
+ 'serverCertificate': cert,
25
+ },
26
+ },
27
+ },
28
+ });
29
+
30
+
31
+ // This is the short form for configuration of a certificate URI:
32
+ player.configure('drm.advanced.com\\.widevine\\.alpha.serverCertificateUri',
33
+ 'https://example.com/service.cert');
34
+
35
+ // This is the long form:
36
+ player.configure({
37
+ drm: {
38
+ advanced: {
39
+ 'com.widevine.alpha': {
40
+ 'serverCertificateUri': 'https://example.com/service.cert',
41
+ },
42
+ },
43
+ },
44
+ });
45
+ ```
package/externs/ima.js CHANGED
@@ -501,6 +501,15 @@ google.ima.dai.api.StreamRequest.prototype.streamActivityMonitorId;
501
501
  google.ima.dai.api.StreamRequest.prototype.format;
502
502
 
503
503
 
504
+ /**
505
+ * @enum {string}
506
+ */
507
+ google.ima.dai.api.StreamRequest.StreamFormat = {
508
+ DASH: 'dash',
509
+ HLS: 'hls',
510
+ };
511
+
512
+
504
513
  /** @const */
505
514
  google.ima.dai.api.VODStreamRequest =
506
515
  class extends google.ima.dai.api.StreamRequest {};
@@ -297,6 +297,13 @@ shaka.extern.Cue = class {
297
297
  */
298
298
  this.fontFamily;
299
299
 
300
+ /**
301
+ * Text shadow color as a CSS text-shadow value.
302
+ * @type {string}
303
+ * @exportDoc
304
+ */
305
+ this.textShadow = '';
306
+
300
307
  /**
301
308
  * Text stroke color as a CSS color, e.g. "#FFFFFF" or "white".
302
309
  * @type {string}
@@ -79,9 +79,9 @@ shaka.ads.ClientSideAdManager = class {
79
79
  });
80
80
 
81
81
  // Notify the SDK when the video has ended, so it can play post-roll ads.
82
- this.video_.onended = () => {
82
+ this.eventManager_.listen(this.video_, 'ended', () => {
83
83
  this.adsLoader_.contentComplete();
84
- };
84
+ });
85
85
  }
86
86
 
87
87
  /**
@@ -402,6 +402,7 @@ shaka.ads.ClientSideAdManager = class {
402
402
  if (this.ad_.isLinear()) {
403
403
  this.adContainer_.setAttribute('ad-active', 'true');
404
404
  this.video_.pause();
405
+ this.ad_.setVolume(this.video_.muted ? 0 : this.video_.volume);
405
406
  }
406
407
  }
407
408
 
@@ -414,7 +415,9 @@ shaka.ads.ClientSideAdManager = class {
414
415
  (new Map()).set('originalEvent', e)));
415
416
  if (this.ad_ && this.ad_.isLinear()) {
416
417
  this.adContainer_.removeAttribute('ad-active');
417
- this.video_.play();
418
+ if (!this.video_.ended) {
419
+ this.video_.play();
420
+ }
418
421
  }
419
422
  }
420
423
  };
@@ -102,11 +102,23 @@ shaka.dash.SegmentTemplate = class {
102
102
  const periodEnd = context.periodInfo.duration ?
103
103
  context.periodInfo.start + context.periodInfo.duration : Infinity;
104
104
 
105
- // Don't fit live content, since it might receive more segments.
106
- // Unless that live content is multi-period; it's safe to fit every period
107
- // but the last one, since only the last period might receive new
108
- // segments.
109
- const shouldFit = periodEnd != Infinity;
105
+ /* When to fit segments. All refactors should honor/update this table:
106
+ *
107
+ * | dynamic | infinite | last | should | notes |
108
+ * | | period | period | fit | |
109
+ * | ------- | -------- | ------ | ------ | ------------------------- |
110
+ * | F | F | X | T | typical VOD |
111
+ * | F | T | X | X | impossible: infinite VOD |
112
+ * | T | F | F | T | typical live, old period |
113
+ * | T | F | T | F | typical IPR |
114
+ * | T | T | F | X | impossible: old, infinite |
115
+ * | T | T | T | F | typical live, new period |
116
+ */
117
+
118
+ // We never fit the final period of dynamic content, which could be
119
+ // infinite live (with no limit to fit to) or IPR (which would expand the
120
+ // most recent segment to the end of the presentation).
121
+ const shouldFit = !(context.dynamic && context.periodInfo.isLastPeriod);
110
122
 
111
123
  if (segmentIndex) {
112
124
  if (shouldFit) {
@@ -2990,6 +2990,7 @@ shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
2990
2990
  'cmfa': 'audio/mp4',
2991
2991
  // MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
2992
2992
  'ts': 'video/mp2t',
2993
+ 'tsa': 'video/mp2t',
2993
2994
 
2994
2995
  // Raw formats:
2995
2996
  'aac': 'audio/aac',
@@ -3027,6 +3028,7 @@ shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_ = {
3027
3028
  'm4f': 'video/mp4',
3028
3029
  'cmfv': 'video/mp4',
3029
3030
  'ts': 'video/mp2t',
3031
+ 'tsv': 'video/mp2t',
3030
3032
  };
3031
3033
 
3032
3034
 
@@ -3041,7 +3043,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
3041
3043
  'm4f': 'application/mp4',
3042
3044
  'cmft': 'application/mp4',
3043
3045
  'vtt': 'text/vtt',
3044
- 'webvtt': 'txt/vtt',
3046
+ 'webvtt': 'text/vtt',
3045
3047
  'ttml': 'application/ttml+xml',
3046
3048
  };
3047
3049
 
@@ -29,9 +29,7 @@ shaka.media.Transmuxer = class {
29
29
  this.muxjs_ = shaka.dependencies.muxjs();
30
30
 
31
31
  /** @private {muxjs.mp4.Transmuxer} */
32
- this.muxTransmuxer_ = new this.muxjs_.mp4.Transmuxer({
33
- 'keepOriginalTimestamps': true,
34
- });
32
+ this.muxTransmuxer_ = null;
35
33
 
36
34
  /** @private {shaka.util.PublicPromise} */
37
35
  this.transmuxPromise_ = null;
@@ -48,16 +46,22 @@ shaka.media.Transmuxer = class {
48
46
  /** @private {boolean} */
49
47
  this.isTransmuxing_ = false;
50
48
 
51
- this.muxTransmuxer_.on('data', (segment) => this.onTransmuxed_(segment));
52
-
53
- this.muxTransmuxer_.on('done', () => this.onTransmuxDone_());
49
+ if (this.muxjs_) {
50
+ this.muxTransmuxer_ = new this.muxjs_.mp4.Transmuxer({
51
+ 'keepOriginalTimestamps': true,
52
+ });
53
+ this.muxTransmuxer_.on('data', (segment) => this.onTransmuxed_(segment));
54
+ this.muxTransmuxer_.on('done', () => this.onTransmuxDone_());
55
+ }
54
56
  }
55
57
 
56
58
  /**
57
59
  * @override
58
60
  */
59
61
  destroy() {
60
- this.muxTransmuxer_.dispose();
62
+ if (this.muxTransmuxer_) {
63
+ this.muxTransmuxer_.dispose();
64
+ }
61
65
  this.muxTransmuxer_ = null;
62
66
  return Promise.resolve();
63
67
  }
@@ -156,6 +160,8 @@ shaka.media.Transmuxer = class {
156
160
  * metadata: !Array.<!Object>}>}
157
161
  */
158
162
  transmux(data) {
163
+ goog.asserts.assert(this.muxTransmuxer_,
164
+ 'mux.js should be available.');
159
165
  goog.asserts.assert(!this.isTransmuxing_,
160
166
  'No transmuxing should be in progress.');
161
167
  this.isTransmuxing_ = true;
@@ -135,15 +135,15 @@ shaka.media.VideoWrapper = class {
135
135
  // calling |Player.load|, meaning that |currentTime| is more meaningful than
136
136
  // |startTime|.
137
137
  //
138
- // Seeking to the current time is a work around for Issue 1298. If we don't
139
- // do this, the video may get stuck and not play.
138
+ // Seeking to the current time is a work around for Issue 1298 and 4888.
139
+ // If we don't do this, the video may get stuck and not play.
140
140
  //
141
141
  // TODO: Need further investigation why it happens. Before and after
142
142
  // setting the current time, video.readyState is 1, video.paused is true,
143
143
  // and video.buffered's TimeRanges length is 0.
144
144
  // See: https://github.com/shaka-project/shaka-player/issues/1298
145
145
  this.mover_.moveTo(
146
- this.video_.currentTime == 0 ?
146
+ (!this.video_.currentTime || this.video_.currentTime == 0) ?
147
147
  startTime :
148
148
  this.video_.currentTime);
149
149
  }
@@ -67,7 +67,7 @@ shaka.offline.indexeddb.StorageMechanism = class {
67
67
  shaka.util.Error.Category.STORAGE,
68
68
  shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT));
69
69
  });
70
- timeOutTimer.tickAfter(2);
70
+ timeOutTimer.tickAfter(5);
71
71
 
72
72
  const open = window.indexedDB.open(name, version);
73
73
  open.onsuccess = (event) => {
package/lib/player.js CHANGED
@@ -2408,6 +2408,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2408
2408
  // goes wrong.
2409
2409
  new shaka.util.Timer(resolve).tickAfter(1);
2410
2410
  });
2411
+ } else if (textTracks.length > 0) {
2412
+ this.isTextVisible_ = true;
2411
2413
  }
2412
2414
 
2413
2415
  // If we have moved on to another piece of content while waiting for
@@ -6663,7 +6665,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6663
6665
  * @define {string} A version number taken from git at compile time.
6664
6666
  * @export
6665
6667
  */
6666
- shaka.Player.version = 'v3.3.16-uncompiled';
6668
+ shaka.Player.version = 'v3.3.18-uncompiled';
6667
6669
 
6668
6670
  // Initialize the deprecation system using the version string we just set
6669
6671
  // on the player.
@@ -8,7 +8,6 @@ goog.provide('shaka.polyfill.MediaCapabilities');
8
8
 
9
9
  goog.require('shaka.log');
10
10
  goog.require('shaka.polyfill');
11
- goog.require('shaka.util.Error');
12
11
  goog.require('shaka.util.Platform');
13
12
 
14
13
 
@@ -45,6 +44,7 @@ shaka.polyfill.MediaCapabilities = class {
45
44
  !shaka.util.Platform.isWebOS() &&
46
45
  !shaka.util.Platform.isTizen() &&
47
46
  !shaka.util.Platform.isEOS() &&
47
+ !shaka.util.Platform.isHisense() &&
48
48
  navigator.mediaCapabilities) {
49
49
  shaka.log.info(
50
50
  'MediaCapabilities: Native mediaCapabilities support found.');
@@ -211,13 +211,8 @@ shaka.polyfill.MediaCapabilities = class {
211
211
  * @private
212
212
  */
213
213
  static canCastDisplayType_(videoConfig) {
214
- if (!(window.cast)) {
215
- shaka.log.error('Expected cast namespace to be available!');
216
- throw new shaka.util.Error(
217
- shaka.util.Error.Severity.CRITICAL,
218
- shaka.util.Error.Category.CAST,
219
- shaka.util.Error.Code.CAST_API_UNAVAILABLE);
220
- } else if (!(cast.__platform__ && cast.__platform__.canDisplayType)) {
214
+ if (!(window.cast &&
215
+ cast.__platform__ && cast.__platform__.canDisplayType)) {
221
216
  shaka.log.warning('Expected cast APIs to be available! Falling back to ' +
222
217
  'MediaSource.isTypeSupported() for type support.');
223
218
  return MediaSource.isTypeSupported(videoConfig.contentType);
@@ -58,14 +58,19 @@ shaka.polyfill.MediaSource = class {
58
58
  // Offsetting the end of the removal range seems to help.
59
59
  // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884
60
60
  shaka.polyfill.MediaSource.patchRemovalRange_();
61
- } else {
62
- shaka.log.info('Patching Safari 13 MSE bugs.');
61
+ } else if (safariVersion <= 15) {
62
+ shaka.log.info('Patching Safari 13 & 14 & 15 MSE bugs.');
63
63
  // Safari 13 does not correctly implement abort() on SourceBuffer.
64
64
  // Calling abort() before appending a segment causes that segment to be
65
65
  // incomplete in the buffer.
66
66
  // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342
67
67
  shaka.polyfill.MediaSource.stubAbort_();
68
68
  }
69
+ } else if (shaka.util.Platform.isEdge()) {
70
+ shaka.log.info('Rejecting TS container.');
71
+ // TS content is broken on Edge in general.
72
+ // See https://github.com/shaka-project/shaka-player/issues/4955
73
+ shaka.polyfill.MediaSource.rejectTsContent_();
69
74
  } else if (shaka.util.Platform.isTizen2() ||
70
75
  shaka.util.Platform.isTizen3() ||
71
76
  shaka.util.Platform.isTizen4()) {
package/lib/text/cue.js CHANGED
@@ -139,6 +139,12 @@ shaka.text.Cue = class {
139
139
  */
140
140
  this.border = '';
141
141
 
142
+ /**
143
+ * @override
144
+ * @exportInterface
145
+ */
146
+ this.textShadow = '';
147
+
142
148
  /**
143
149
  * @override
144
150
  * @exportInterface
@@ -480,6 +480,7 @@ shaka.text.UITextDisplayer = class {
480
480
  wrapper = shaka.util.Dom.createHTMLElement('span');
481
481
  wrapper.classList.add('shaka-text-wrapper');
482
482
  wrapper.style.backgroundColor = cue.backgroundColor;
483
+ wrapper.style.lineHeight = 'normal';
483
484
  cueElement.appendChild(wrapper);
484
485
  }
485
486
 
@@ -529,6 +530,7 @@ shaka.text.UITextDisplayer = class {
529
530
  style.paddingRight =
530
531
  shaka.text.UITextDisplayer.convertLengthValue_(
531
532
  cue.linePadding, cue, this.videoContainer_);
533
+ style.textShadow = cue.textShadow;
532
534
 
533
535
  if (cue.backgroundImage) {
534
536
  style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
@@ -622,12 +624,7 @@ shaka.text.UITextDisplayer = class {
622
624
  // TODO: Implement lineAlignment of 'CENTER'.
623
625
  if (cue.line != null) {
624
626
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
625
- // When setting absolute positioning, you need to set x/y/width/height
626
- // so the element is positioned correctly. Set these as default and
627
- // other settings will override them.
628
627
  style.position = 'absolute';
629
- style.left = '0';
630
- style.top = '0';
631
628
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
632
629
  style.width = '100%';
633
630
  if (cue.lineAlign == Cue.lineAlign.START) {