ts-video-player 0.1.4 → 0.1.6
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.
- package/dist/cdn.min.js +42 -42
- package/dist/{chunk-hfd9k46d.js → chunk-fvz33zn4.js} +78 -2
- package/dist/core/cover.d.ts +24 -0
- package/dist/elements/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/player.d.ts +17 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/core/cover.ts +46 -0
- package/src/elements/video-player.ts +3 -0
- package/src/player.ts +72 -1
- package/src/providers/youtube.ts +8 -2
- package/src/types.ts +2 -0
|
@@ -666,7 +666,9 @@ class YouTubeProvider2 extends BaseProvider2 {
|
|
|
666
666
|
});
|
|
667
667
|
this.iframe = this.player.getIframe();
|
|
668
668
|
this.iframe.className = "ts-video-player__media";
|
|
669
|
-
this.iframe.style.cssText = "position:absolute;
|
|
669
|
+
this.iframe.style.cssText = "position:absolute;left:0;top:-100%;width:100%;height:300%;border:0";
|
|
670
|
+
if (this.iframe.parentElement)
|
|
671
|
+
this.iframe.parentElement.style.overflow = "hidden";
|
|
670
672
|
}
|
|
671
673
|
async loadYouTubeAPI() {
|
|
672
674
|
if (window.YT?.Player)
|
|
@@ -2009,6 +2011,20 @@ function getPreconnectHints2(src, loaders = defaultLoaders2) {
|
|
|
2009
2011
|
return loader?.preconnect?.() || [];
|
|
2010
2012
|
}
|
|
2011
2013
|
|
|
2014
|
+
// src/core/cover.ts
|
|
2015
|
+
function embedCover(state, dismissed) {
|
|
2016
|
+
const embed = state.providerType === "youtube" || state.providerType === "vimeo";
|
|
2017
|
+
const waiting = !state.started || state.ended;
|
|
2018
|
+
if (!embed || !waiting || state.error || dismissed)
|
|
2019
|
+
return { show: false, image: "" };
|
|
2020
|
+
if (state.poster)
|
|
2021
|
+
return { show: true, image: state.poster };
|
|
2022
|
+
const src = state.src;
|
|
2023
|
+
const url = typeof src === "string" ? src : src?.src || "";
|
|
2024
|
+
const youtubeId = state.providerType === "youtube" ? extractYouTubeId2(url) : null;
|
|
2025
|
+
return { show: true, image: youtubeId ? `https://i.ytimg.com/vi/${youtubeId}/hqdefault.jpg` : "" };
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2012
2028
|
// src/player.ts
|
|
2013
2029
|
class Player2 {
|
|
2014
2030
|
_el;
|
|
@@ -2021,6 +2037,9 @@ class Player2 {
|
|
|
2021
2037
|
_destroyed = false;
|
|
2022
2038
|
_sourceLoadId = 0;
|
|
2023
2039
|
_announcer = null;
|
|
2040
|
+
_cover = null;
|
|
2041
|
+
_coverDismissed = false;
|
|
2042
|
+
_coverTimer = null;
|
|
2024
2043
|
constructor(container, options = {}) {
|
|
2025
2044
|
if (typeof container === "string") {
|
|
2026
2045
|
const el = document.querySelector(container);
|
|
@@ -2068,6 +2087,7 @@ class Player2 {
|
|
|
2068
2087
|
mediaContainer.className = "ts-video-player__container";
|
|
2069
2088
|
mediaContainer.style.cssText = "position:relative;width:100%;height:100%";
|
|
2070
2089
|
this._el.appendChild(mediaContainer);
|
|
2090
|
+
this.setupCover();
|
|
2071
2091
|
if (this._options.announcements !== false) {
|
|
2072
2092
|
const liveRegion = document.createElement("div");
|
|
2073
2093
|
liveRegion.className = "ts-video-player__announcer";
|
|
@@ -2081,6 +2101,53 @@ class Player2 {
|
|
|
2081
2101
|
const _el = this._el;
|
|
2082
2102
|
_el.__videoPlayer = this;
|
|
2083
2103
|
}
|
|
2104
|
+
setupCover() {
|
|
2105
|
+
const cover = document.createElement("button");
|
|
2106
|
+
cover.type = "button";
|
|
2107
|
+
cover.className = "ts-video-player__cover";
|
|
2108
|
+
cover.setAttribute("aria-label", `Play ${this._options.title || "video"}`);
|
|
2109
|
+
cover.hidden = true;
|
|
2110
|
+
cover.style.cssText = "position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:#000 center/cover no-repeat;cursor:pointer;-webkit-tap-highlight-color:transparent";
|
|
2111
|
+
const button = document.createElement("span");
|
|
2112
|
+
button.setAttribute("aria-hidden", "true");
|
|
2113
|
+
button.style.cssText = "display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.92);box-shadow:0 4px 18px rgba(0,0,0,0.35)";
|
|
2114
|
+
const triangle = document.createElement("span");
|
|
2115
|
+
triangle.style.cssText = "width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)";
|
|
2116
|
+
button.appendChild(triangle);
|
|
2117
|
+
cover.appendChild(button);
|
|
2118
|
+
cover.addEventListener("click", (event) => {
|
|
2119
|
+
event.stopPropagation();
|
|
2120
|
+
this.play().catch(() => {});
|
|
2121
|
+
if (this._coverTimer)
|
|
2122
|
+
clearTimeout(this._coverTimer);
|
|
2123
|
+
this._coverTimer = setTimeout(() => {
|
|
2124
|
+
if (this.state.started)
|
|
2125
|
+
return;
|
|
2126
|
+
this._coverDismissed = true;
|
|
2127
|
+
this.updateCover(this.state);
|
|
2128
|
+
}, 2000);
|
|
2129
|
+
});
|
|
2130
|
+
this._el.appendChild(cover);
|
|
2131
|
+
this._cover = cover;
|
|
2132
|
+
this._cleanupFns.push(() => {
|
|
2133
|
+
if (this._coverTimer)
|
|
2134
|
+
clearTimeout(this._coverTimer);
|
|
2135
|
+
cover.remove();
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
updateCover(state) {
|
|
2139
|
+
const cover = this._cover;
|
|
2140
|
+
if (!cover)
|
|
2141
|
+
return;
|
|
2142
|
+
const { show, image } = embedCover(state, this._coverDismissed);
|
|
2143
|
+
if (show) {
|
|
2144
|
+
const background = image ? `url("${image.replace(/["\\]/g, "")}")` : "none";
|
|
2145
|
+
if (cover.style.backgroundImage !== background)
|
|
2146
|
+
cover.style.backgroundImage = background;
|
|
2147
|
+
}
|
|
2148
|
+
cover.hidden = !show;
|
|
2149
|
+
cover.style.display = show ? "flex" : "none";
|
|
2150
|
+
}
|
|
2084
2151
|
setupState() {
|
|
2085
2152
|
this._store.batch({
|
|
2086
2153
|
volume: this._options.volume ?? 1,
|
|
@@ -2095,6 +2162,7 @@ class Player2 {
|
|
|
2095
2162
|
});
|
|
2096
2163
|
const unsubscribe = this._store.subscribe((state) => {
|
|
2097
2164
|
this.updateAttributes(state);
|
|
2165
|
+
this.updateCover(state);
|
|
2098
2166
|
});
|
|
2099
2167
|
this._cleanupFns.push(unsubscribe);
|
|
2100
2168
|
}
|
|
@@ -2238,11 +2306,13 @@ class Player2 {
|
|
|
2238
2306
|
if (sources.length === 0) {
|
|
2239
2307
|
return;
|
|
2240
2308
|
}
|
|
2309
|
+
this._coverDismissed = false;
|
|
2241
2310
|
this._store.batch({
|
|
2242
2311
|
sources,
|
|
2243
2312
|
src: sources[0],
|
|
2244
2313
|
loadingState: "loading",
|
|
2245
|
-
error: null
|
|
2314
|
+
error: null,
|
|
2315
|
+
started: false
|
|
2246
2316
|
});
|
|
2247
2317
|
this._events.emit("sourceschange", sources);
|
|
2248
2318
|
const candidates = findSourceCandidates(sources, this._loaders);
|
|
@@ -2311,6 +2381,9 @@ class Player2 {
|
|
|
2311
2381
|
error: { code: 4, message: "Failed to load all compatible sources", details: lastError }
|
|
2312
2382
|
});
|
|
2313
2383
|
}
|
|
2384
|
+
setPoster(url) {
|
|
2385
|
+
this._store.set("poster", url);
|
|
2386
|
+
}
|
|
2314
2387
|
getSrc() {
|
|
2315
2388
|
return this.state.src;
|
|
2316
2389
|
}
|
|
@@ -2629,6 +2702,9 @@ class VideoPlayerElement2 extends HTMLElementBase {
|
|
|
2629
2702
|
if (value)
|
|
2630
2703
|
this._player.setSrc(value);
|
|
2631
2704
|
break;
|
|
2705
|
+
case "poster":
|
|
2706
|
+
this._player.setPoster(value || "");
|
|
2707
|
+
break;
|
|
2632
2708
|
case "muted":
|
|
2633
2709
|
this._player.setMuted(value !== null);
|
|
2634
2710
|
break;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The poster over an embedded video until it plays: when to show it, and
|
|
3
|
+
* with which image.
|
|
4
|
+
*
|
|
5
|
+
* @module core/cover
|
|
6
|
+
*/
|
|
7
|
+
import type { PlayerState } from '../types';
|
|
8
|
+
export interface EmbedCover {
|
|
9
|
+
show: boolean;
|
|
10
|
+
/** The poster to draw, or '' for a plain black cover. */
|
|
11
|
+
image: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A YouTube or Vimeo embed draws its own chrome before playback (a title bar,
|
|
15
|
+
* a play button, "Watch on YouTube", and on iOS the system's controls for the
|
|
16
|
+
* embed's own video), none of which can be styled from outside the iframe.
|
|
17
|
+
* Until the first play, and again once it ends (where YouTube shows other
|
|
18
|
+
* videos), the player covers it with its poster instead: the one the page
|
|
19
|
+
* gave it, or else YouTube's thumbnail for the video.
|
|
20
|
+
*
|
|
21
|
+
* `dismissed` is set when a tap on the cover did not start playback, so the
|
|
22
|
+
* embed's own button is left reachable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function embedCover(state: Pick<PlayerState, 'providerType' | 'started' | 'ended' | 'error' | 'poster' | 'src'>, dismissed: boolean): EmbedCover;
|
package/dist/elements/index.js
CHANGED
package/dist/index.js
CHANGED
package/dist/player.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare class Player implements IPlayer {
|
|
|
20
20
|
private _destroyed;
|
|
21
21
|
private _sourceLoadId;
|
|
22
22
|
private _announcer;
|
|
23
|
+
private _cover;
|
|
24
|
+
private _coverDismissed;
|
|
25
|
+
private _coverTimer;
|
|
23
26
|
/**
|
|
24
27
|
* Create a new player instance
|
|
25
28
|
*/
|
|
@@ -29,6 +32,18 @@ export declare class Player implements IPlayer {
|
|
|
29
32
|
get provider(): Provider | null;
|
|
30
33
|
get ready(): boolean;
|
|
31
34
|
private setupElement;
|
|
35
|
+
/**
|
|
36
|
+
* The poster over an embedded video until it plays.
|
|
37
|
+
*
|
|
38
|
+
* A YouTube or Vimeo embed draws its own chrome before playback: a title
|
|
39
|
+
* bar, a play button, "Watch on YouTube", and on iOS the system's controls
|
|
40
|
+
* for the embed's own video. None of it can be styled from outside the
|
|
41
|
+
* iframe, and it stacks under the player's controls. Until the first play,
|
|
42
|
+
* the player shows its poster and one play button over the embed instead;
|
|
43
|
+
* once the video plays the embed's chrome is gone and the cover lifts.
|
|
44
|
+
*/
|
|
45
|
+
private setupCover;
|
|
46
|
+
private updateCover;
|
|
32
47
|
private setupState;
|
|
33
48
|
private setupKeyboard;
|
|
34
49
|
private setupActivity;
|
|
@@ -42,6 +57,8 @@ export declare class Player implements IPlayer {
|
|
|
42
57
|
private announce;
|
|
43
58
|
destroy(): void;
|
|
44
59
|
setSrc(src: Src | Src[]): Promise<void>;
|
|
60
|
+
/** The image shown before playback, and on an embed's cover. */
|
|
61
|
+
setPoster(url: string): void;
|
|
45
62
|
getSrc(): Src | null;
|
|
46
63
|
private attachProviderEvents;
|
|
47
64
|
play(): Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -632,6 +632,8 @@ export interface Player {
|
|
|
632
632
|
destroy(): void;
|
|
633
633
|
setSrc(src: Src | Src[]): void;
|
|
634
634
|
getSrc(): Src | null;
|
|
635
|
+
/** The image shown before playback, and on an embed's cover */
|
|
636
|
+
setPoster(url: string): void;
|
|
635
637
|
play(): Promise<void>;
|
|
636
638
|
pause(): void;
|
|
637
639
|
togglePlay(): void;
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The poster over an embedded video until it plays: when to show it, and
|
|
3
|
+
* with which image.
|
|
4
|
+
*
|
|
5
|
+
* @module core/cover
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PlayerState } from '../types'
|
|
9
|
+
import { extractYouTubeId } from '../providers/youtube'
|
|
10
|
+
|
|
11
|
+
export interface EmbedCover {
|
|
12
|
+
show: boolean
|
|
13
|
+
/** The poster to draw, or '' for a plain black cover. */
|
|
14
|
+
image: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A YouTube or Vimeo embed draws its own chrome before playback (a title bar,
|
|
19
|
+
* a play button, "Watch on YouTube", and on iOS the system's controls for the
|
|
20
|
+
* embed's own video), none of which can be styled from outside the iframe.
|
|
21
|
+
* Until the first play, and again once it ends (where YouTube shows other
|
|
22
|
+
* videos), the player covers it with its poster instead: the one the page
|
|
23
|
+
* gave it, or else YouTube's thumbnail for the video.
|
|
24
|
+
*
|
|
25
|
+
* `dismissed` is set when a tap on the cover did not start playback, so the
|
|
26
|
+
* embed's own button is left reachable.
|
|
27
|
+
*/
|
|
28
|
+
export function embedCover(
|
|
29
|
+
state: Pick<PlayerState, 'providerType' | 'started' | 'ended' | 'error' | 'poster' | 'src'>,
|
|
30
|
+
dismissed: boolean,
|
|
31
|
+
): EmbedCover {
|
|
32
|
+
const embed = state.providerType === 'youtube' || state.providerType === 'vimeo'
|
|
33
|
+
// Back over the embed when the video ends, where YouTube would otherwise
|
|
34
|
+
// fill the frame with suggestions of other videos.
|
|
35
|
+
const waiting = !state.started || state.ended
|
|
36
|
+
if (!embed || !waiting || state.error || dismissed)
|
|
37
|
+
return { show: false, image: '' }
|
|
38
|
+
|
|
39
|
+
if (state.poster)
|
|
40
|
+
return { show: true, image: state.poster }
|
|
41
|
+
|
|
42
|
+
const src = state.src
|
|
43
|
+
const url = typeof src === 'string' ? src : src?.src || ''
|
|
44
|
+
const youtubeId = state.providerType === 'youtube' ? extractYouTubeId(url) : null
|
|
45
|
+
return { show: true, image: youtubeId ? `https://i.ytimg.com/vi/${youtubeId}/hqdefault.jpg` : '' }
|
|
46
|
+
}
|
package/src/player.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { EventEmitter, createKeyboardHandler, createActivityDetector, type Keybo
|
|
|
21
21
|
import { createMediaSession, type MediaSessionOptions } from './core/media-session'
|
|
22
22
|
import { lockOrientation, unlockOrientation } from './core/orientation'
|
|
23
23
|
import { defaultLoaders, findSourceCandidates } from './providers'
|
|
24
|
+
import { embedCover } from './core/cover'
|
|
24
25
|
|
|
25
26
|
// =============================================================================
|
|
26
27
|
// Player Class
|
|
@@ -40,6 +41,9 @@ export class Player implements IPlayer {
|
|
|
40
41
|
private _destroyed = false
|
|
41
42
|
private _sourceLoadId = 0
|
|
42
43
|
private _announcer: HTMLElement | null = null
|
|
44
|
+
private _cover: HTMLButtonElement | null = null
|
|
45
|
+
private _coverDismissed = false
|
|
46
|
+
private _coverTimer: ReturnType<typeof setTimeout> | null = null
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* Create a new player instance
|
|
@@ -114,6 +118,8 @@ export class Player implements IPlayer {
|
|
|
114
118
|
mediaContainer.style.cssText = 'position:relative;width:100%;height:100%'
|
|
115
119
|
this._el.appendChild(mediaContainer)
|
|
116
120
|
|
|
121
|
+
this.setupCover()
|
|
122
|
+
|
|
117
123
|
// Create ARIA live region for screen reader announcements
|
|
118
124
|
if (this._options.announcements !== false) {
|
|
119
125
|
const liveRegion = document.createElement('div')
|
|
@@ -132,6 +138,63 @@ export class Player implements IPlayer {
|
|
|
132
138
|
_el.__videoPlayer = this
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The poster over an embedded video until it plays.
|
|
143
|
+
*
|
|
144
|
+
* A YouTube or Vimeo embed draws its own chrome before playback: a title
|
|
145
|
+
* bar, a play button, "Watch on YouTube", and on iOS the system's controls
|
|
146
|
+
* for the embed's own video. None of it can be styled from outside the
|
|
147
|
+
* iframe, and it stacks under the player's controls. Until the first play,
|
|
148
|
+
* the player shows its poster and one play button over the embed instead;
|
|
149
|
+
* once the video plays the embed's chrome is gone and the cover lifts.
|
|
150
|
+
*/
|
|
151
|
+
private setupCover(): void {
|
|
152
|
+
const cover = document.createElement('button')
|
|
153
|
+
cover.type = 'button'
|
|
154
|
+
cover.className = 'ts-video-player__cover'
|
|
155
|
+
cover.setAttribute('aria-label', `Play ${this._options.title || 'video'}`)
|
|
156
|
+
cover.hidden = true
|
|
157
|
+
cover.style.cssText = 'position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:#000 center/cover no-repeat;cursor:pointer;-webkit-tap-highlight-color:transparent'
|
|
158
|
+
const button = document.createElement('span')
|
|
159
|
+
button.setAttribute('aria-hidden', 'true')
|
|
160
|
+
button.style.cssText = 'display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.92);box-shadow:0 4px 18px rgba(0,0,0,0.35)'
|
|
161
|
+
const triangle = document.createElement('span')
|
|
162
|
+
triangle.style.cssText = 'width:22px;height:24px;margin-left:5px;background:#0f172a;clip-path:polygon(0 0,100% 50%,0 100%)'
|
|
163
|
+
button.appendChild(triangle)
|
|
164
|
+
cover.appendChild(button)
|
|
165
|
+
cover.addEventListener('click', (event) => {
|
|
166
|
+
event.stopPropagation()
|
|
167
|
+
void this.play().catch(() => {})
|
|
168
|
+
// Some browsers only let a tap inside the embed start it. If this one
|
|
169
|
+
// did not, get out of the way so the embed's own button can.
|
|
170
|
+
if (this._coverTimer) clearTimeout(this._coverTimer)
|
|
171
|
+
this._coverTimer = setTimeout(() => {
|
|
172
|
+
if (this.state.started) return
|
|
173
|
+
this._coverDismissed = true
|
|
174
|
+
this.updateCover(this.state)
|
|
175
|
+
}, 2000)
|
|
176
|
+
})
|
|
177
|
+
this._el.appendChild(cover)
|
|
178
|
+
this._cover = cover
|
|
179
|
+
this._cleanupFns.push(() => {
|
|
180
|
+
if (this._coverTimer) clearTimeout(this._coverTimer)
|
|
181
|
+
cover.remove()
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private updateCover(state: PlayerState): void {
|
|
186
|
+
const cover = this._cover
|
|
187
|
+
if (!cover) return
|
|
188
|
+
const { show, image } = embedCover(state, this._coverDismissed)
|
|
189
|
+
if (show) {
|
|
190
|
+
const background = image ? `url("${image.replace(/["\\]/g, '')}")` : 'none'
|
|
191
|
+
if (cover.style.backgroundImage !== background) cover.style.backgroundImage = background
|
|
192
|
+
}
|
|
193
|
+
// `hidden` alone would not do it: the inline display wins over it.
|
|
194
|
+
cover.hidden = !show
|
|
195
|
+
cover.style.display = show ? 'flex' : 'none'
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
private setupState(): void {
|
|
136
199
|
// Initial state from options
|
|
137
200
|
this._store.batch({
|
|
@@ -150,6 +213,7 @@ export class Player implements IPlayer {
|
|
|
150
213
|
const unsubscribe = this._store.subscribe((state) => {
|
|
151
214
|
// Update DOM attributes
|
|
152
215
|
this.updateAttributes(state)
|
|
216
|
+
this.updateCover(state)
|
|
153
217
|
})
|
|
154
218
|
|
|
155
219
|
this._cleanupFns.push(unsubscribe)
|
|
@@ -366,12 +430,14 @@ export class Player implements IPlayer {
|
|
|
366
430
|
return
|
|
367
431
|
}
|
|
368
432
|
|
|
369
|
-
// Update state
|
|
433
|
+
// Update state. A new source has not played yet, so its cover returns.
|
|
434
|
+
this._coverDismissed = false
|
|
370
435
|
this._store.batch({
|
|
371
436
|
sources,
|
|
372
437
|
src: sources[0],
|
|
373
438
|
loadingState: 'loading',
|
|
374
439
|
error: null,
|
|
440
|
+
started: false,
|
|
375
441
|
})
|
|
376
442
|
|
|
377
443
|
this._events.emit('sourceschange', sources)
|
|
@@ -451,6 +517,11 @@ export class Player implements IPlayer {
|
|
|
451
517
|
})
|
|
452
518
|
}
|
|
453
519
|
|
|
520
|
+
/** The image shown before playback, and on an embed's cover. */
|
|
521
|
+
setPoster(url: string): void {
|
|
522
|
+
this._store.set('poster', url)
|
|
523
|
+
}
|
|
524
|
+
|
|
454
525
|
getSrc(): Src | null {
|
|
455
526
|
return this.state.src
|
|
456
527
|
}
|
package/src/providers/youtube.ts
CHANGED
|
@@ -254,8 +254,14 @@ export class YouTubeProvider extends BaseProvider {
|
|
|
254
254
|
// Get iframe reference
|
|
255
255
|
this.iframe = this.player.getIframe()
|
|
256
256
|
this.iframe.className = 'ts-video-player__media'
|
|
257
|
-
//
|
|
258
|
-
|
|
257
|
+
// Three times the player's height, centred, and clipped by the player.
|
|
258
|
+
// YouTube letterboxes the video in the middle of its frame, so the video
|
|
259
|
+
// still fills the player, while the chrome it draws at the frame's edges
|
|
260
|
+
// during playback (the title bar and channel avatar at the top, the
|
|
261
|
+
// YouTube logo and "More videos" at the bottom) falls outside the clip.
|
|
262
|
+
// It cannot be styled from outside the iframe any other way.
|
|
263
|
+
this.iframe.style.cssText = 'position:absolute;left:0;top:-100%;width:100%;height:300%;border:0'
|
|
264
|
+
if (this.iframe.parentElement) this.iframe.parentElement.style.overflow = 'hidden'
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
private async loadYouTubeAPI(): Promise<void> {
|
package/src/types.ts
CHANGED