@mattebox/player-cast 0.1.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +24 -0
  3. package/README.md +54 -0
  4. package/dist/cast-button.d.ts +13 -0
  5. package/dist/cast-button.d.ts.map +1 -0
  6. package/dist/cast-button.js +65 -0
  7. package/dist/cast-button.js.map +1 -0
  8. package/dist/cast-screen.d.ts +24 -0
  9. package/dist/cast-screen.d.ts.map +1 -0
  10. package/dist/cast-screen.js +148 -0
  11. package/dist/cast-screen.js.map +1 -0
  12. package/dist/cast.d.ts +77 -0
  13. package/dist/cast.d.ts.map +1 -0
  14. package/dist/cast.js +343 -0
  15. package/dist/cast.js.map +1 -0
  16. package/dist/cdn/mattebox-player-cast.min.js +91 -0
  17. package/dist/component.d.ts +48 -0
  18. package/dist/component.d.ts.map +1 -0
  19. package/dist/component.js +89 -0
  20. package/dist/component.js.map +1 -0
  21. package/dist/es2015/cast-button.js +59 -0
  22. package/dist/es2015/cast-screen.js +151 -0
  23. package/dist/es2015/cast.js +299 -0
  24. package/dist/es2015/component.js +84 -0
  25. package/dist/es2015/host.js +19 -0
  26. package/dist/es2015/icons.js +23 -0
  27. package/dist/es2015/index.js +17 -0
  28. package/dist/es2015/shared.js +125 -0
  29. package/dist/es2015/slider.js +154 -0
  30. package/dist/host.d.ts +14 -0
  31. package/dist/host.d.ts.map +1 -0
  32. package/dist/host.js +19 -0
  33. package/dist/host.js.map +1 -0
  34. package/dist/icons.d.ts +33 -0
  35. package/dist/icons.d.ts.map +1 -0
  36. package/dist/icons.js +52 -0
  37. package/dist/icons.js.map +1 -0
  38. package/dist/index.d.ts +22 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +21 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/shared.d.ts +29 -0
  43. package/dist/shared.d.ts.map +1 -0
  44. package/dist/shared.js +125 -0
  45. package/dist/shared.js.map +1 -0
  46. package/dist/slider.d.ts +28 -0
  47. package/dist/slider.d.ts.map +1 -0
  48. package/dist/slider.js +165 -0
  49. package/dist/slider.js.map +1 -0
  50. package/package.json +59 -0
@@ -0,0 +1,59 @@
1
+ import { cast } from "./cast.js";
2
+ import { Component } from "./component.js";
3
+ import { icon } from "./icons.js";
4
+ import { BUTTON_STYLE, iconSlots, show, style } from "./shared.js";
5
+ //#region src/cast-button.ts
6
+ const STATES = ["default", "active"];
7
+ var MbxCastButton = class extends Component {
8
+ static get observedAttributes() {
9
+ return ["label-start", "label-stop"];
10
+ }
11
+ constructor() {
12
+ super();
13
+ this.api = null;
14
+ const root = this.attachShadow({ mode: "open" });
15
+ this.button = document.createElement("button");
16
+ this.button.type = "button";
17
+ this.button.setAttribute("part", "button");
18
+ this.slots = iconSlots(STATES, (state) => icon(state === "active" ? "cast-active" : "cast"));
19
+ for (const state of STATES) this.button.append(this.slots[state]);
20
+ this.button.addEventListener("click", () => {
21
+ const api = this.api;
22
+ if (api === null) return;
23
+ if (api.state() === "casting") api.stop();
24
+ else api.start(this);
25
+ });
26
+ root.append(style(BUTTON_STYLE), this.button);
27
+ }
28
+ attach(player) {
29
+ const api = cast(player);
30
+ this.api = api;
31
+ api.connect(this.getAttribute("receiver"), this.getAttribute("sdk"));
32
+ this.keep(api.watch(() => {
33
+ this.render();
34
+ }));
35
+ this.follow(player, () => {
36
+ this.render();
37
+ });
38
+ }
39
+ detach() {
40
+ var _this$api;
41
+ (_this$api = this.api) === null || _this$api === void 0 || _this$api.disconnect();
42
+ this.api = null;
43
+ }
44
+ render() {
45
+ var _player$player, _this$getAttribute, _this$getAttribute2;
46
+ const player = this.player;
47
+ const api = this.api;
48
+ if (player === null || api === null) return;
49
+ const state = api.state();
50
+ const active = state === "casting";
51
+ this.hidden = state === "unavailable";
52
+ this.button.disabled = !active && ((_player$player = player.player) === null || _player$player === void 0 ? void 0 : _player$player.session) == null;
53
+ this.button.toggleAttribute("aria-busy", state === "connecting");
54
+ show(this.slots, active ? "active" : "default");
55
+ this.button.setAttribute("aria-label", active ? (_this$getAttribute = this.getAttribute("label-stop")) !== null && _this$getAttribute !== void 0 ? _this$getAttribute : "Stop casting" : (_this$getAttribute2 = this.getAttribute("label-start")) !== null && _this$getAttribute2 !== void 0 ? _this$getAttribute2 : "Cast");
56
+ }
57
+ };
58
+ //#endregion
59
+ export { MbxCastButton };
@@ -0,0 +1,151 @@
1
+ import { cast } from "./cast.js";
2
+ import { Component } from "./component.js";
3
+ import { icon } from "./icons.js";
4
+ import { BUTTON_STYLE, SLIDER_STYLE, describe, el, fill, format, iconSlots, show, style } from "./shared.js";
5
+ import { slider } from "./slider.js";
6
+ //#region src/cast-screen.ts
7
+ const STATES = ["play", "pause"];
8
+ const STEP = 5;
9
+ const PAGE = 30;
10
+ const STYLE = `${BUTTON_STYLE}
11
+ ${SLIDER_STYLE}
12
+ :host {
13
+ position: absolute;
14
+ inset: 0;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: 16px;
19
+ background: rgba(0, 0, 0, 0.85);
20
+ color: var(--mbx-text);
21
+ font: 400 15px/1.4 var(--mbx-font);
22
+ text-align: center;
23
+ }
24
+ :host([hidden]) { display: none; }
25
+ [part~="box"] { display: flex; flex-direction: column; align-items: center; gap: 12px; width: min(100%, 420px); }
26
+ [part~="title"] { font-size: 18px; font-weight: 600; }
27
+ [part~="row"] { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; }
28
+ [part~="slider"] { flex: 1; }
29
+ [part~="time"] { font-variant-numeric: tabular-nums; color: var(--mbx-muted); font-size: 13px; }
30
+ [part~="stop"] {
31
+ height: 36px;
32
+ padding: 0 16px;
33
+ font: inherit;
34
+ font-weight: 600;
35
+ color: inherit;
36
+ background: transparent;
37
+ border: 1px solid currentColor;
38
+ border-radius: var(--mbx-radius);
39
+ cursor: pointer;
40
+ }
41
+ [part~="stop"]:focus-visible { outline: 2px solid var(--mbx-accent); outline-offset: -2px; }
42
+ [hidden] { display: none; }
43
+ `;
44
+ var MbxCastScreen = class extends Component {
45
+ static get observedAttributes() {
46
+ return [
47
+ "label",
48
+ "label-stop",
49
+ "label-play",
50
+ "label-pause",
51
+ "label-seek"
52
+ ];
53
+ }
54
+ constructor() {
55
+ super();
56
+ this.api = null;
57
+ this.held = null;
58
+ const root = this.attachShadow({ mode: "open" });
59
+ const box = el("div", "box");
60
+ this.title_ = el("div", "title");
61
+ this.play = document.createElement("button");
62
+ this.play.type = "button";
63
+ this.play.setAttribute("part", "button play");
64
+ this.slots = iconSlots(STATES, icon);
65
+ for (const state of STATES) this.play.append(this.slots[state]);
66
+ this.play.addEventListener("click", () => {
67
+ var _this$api;
68
+ (_this$api = this.api) === null || _this$api === void 0 || _this$api.playPause();
69
+ });
70
+ this.seek = slider({
71
+ step: () => STEP,
72
+ page: () => PAGE,
73
+ onInput: (value) => {
74
+ var _this$api2;
75
+ if (this.seek.dragging()) {
76
+ this.held = value;
77
+ this.paint();
78
+ return;
79
+ }
80
+ this.held = null;
81
+ (_this$api2 = this.api) === null || _this$api2 === void 0 || _this$api2.seek(value);
82
+ },
83
+ onDrag: (on) => {
84
+ this.toggleAttribute("dragging", on);
85
+ if (!on) this.held = null;
86
+ }
87
+ });
88
+ this.time = el("span", "time");
89
+ this.row = el("div", "row");
90
+ this.row.append(this.play, this.seek.root, this.time);
91
+ this.stop = el("button", "stop");
92
+ this.stop.type = "button";
93
+ this.stop.addEventListener("click", () => {
94
+ var _this$api3;
95
+ (_this$api3 = this.api) === null || _this$api3 === void 0 || _this$api3.stop();
96
+ });
97
+ box.append(this.title_, this.row, this.stop);
98
+ root.append(style(STYLE), box);
99
+ }
100
+ /** Hidden before attaching: a constructor must not add attributes. */
101
+ connectedCallback() {
102
+ if (this.player === null) this.hidden = true;
103
+ super.connectedCallback();
104
+ }
105
+ attach(player) {
106
+ const api = cast(player);
107
+ this.api = api;
108
+ this.keep(api.watch(() => {
109
+ this.render();
110
+ }));
111
+ this.render();
112
+ }
113
+ detach() {
114
+ this.api = null;
115
+ this.hidden = true;
116
+ }
117
+ /** The slider and the time, from the receiver or from the held thumb. */
118
+ paint() {
119
+ var _this$held;
120
+ const api = this.api;
121
+ if (api === null) return;
122
+ const remote = api.remote();
123
+ const seekable = remote.loaded && Number.isFinite(remote.duration);
124
+ this.seek.root.hidden = !seekable;
125
+ this.time.hidden = !seekable;
126
+ if (!seekable) return;
127
+ const at = (_this$held = this.held) !== null && _this$held !== void 0 ? _this$held : remote.time;
128
+ this.seek.range(0, remote.duration);
129
+ this.seek.set(at, describe(at, remote.duration));
130
+ this.time.textContent = `${format(at)} / ${format(remote.duration)}`;
131
+ }
132
+ render() {
133
+ var _this$getAttribute, _this$getAttribute2, _this$getAttribute3, _this$getAttribute4, _api$device;
134
+ const api = this.api;
135
+ if (api === null) return;
136
+ const remote = api.remote();
137
+ const state = remote.loaded && !remote.paused ? "pause" : "play";
138
+ show(this.slots, state);
139
+ this.play.setAttribute("aria-label", (_this$getAttribute = this.getAttribute(`label-${state}`)) !== null && _this$getAttribute !== void 0 ? _this$getAttribute : state === "play" ? "Play" : "Pause");
140
+ this.seek.label((_this$getAttribute2 = this.getAttribute("label-seek")) !== null && _this$getAttribute2 !== void 0 ? _this$getAttribute2 : "Seek");
141
+ this.stop.textContent = (_this$getAttribute3 = this.getAttribute("label-stop")) !== null && _this$getAttribute3 !== void 0 ? _this$getAttribute3 : "Stop casting";
142
+ const casting = api.state() === "casting";
143
+ this.hidden = !casting;
144
+ if (!casting) return;
145
+ this.title_.textContent = fill((_this$getAttribute4 = this.getAttribute("label")) !== null && _this$getAttribute4 !== void 0 ? _this$getAttribute4 : "Casting to {device}", { device: (_api$device = api.device()) !== null && _api$device !== void 0 ? _api$device : "" });
146
+ this.play.disabled = !remote.loaded;
147
+ this.paint();
148
+ }
149
+ };
150
+ //#endregion
151
+ export { MbxCastScreen };
@@ -0,0 +1,299 @@
1
+ //#region src/cast.ts
2
+ /** Whether the session is live: the engine's `live` namespace, once it has an edge. */
3
+ function live(engine) {
4
+ const api = engine === null || engine === void 0 ? void 0 : engine.live;
5
+ return api !== void 0 && api.edge !== null;
6
+ }
7
+ /** The Default Media Receiver: Google's player, which plays HLS and DASH by itself. */
8
+ const DEFAULT_RECEIVER = "CC1AD845";
9
+ const SDK_URL = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
10
+ let framework = null;
11
+ let configured = null;
12
+ let remotePlayer = null;
13
+ let remoteController = null;
14
+ const watchers = /* @__PURE__ */ new Set();
15
+ /** The player whose session this is, so two players on a page do not both take a session. */
16
+ let owner = null;
17
+ /** The receiver's last known playback, read while it reports and kept once it stops. */
18
+ const NOTHING = {
19
+ loaded: false,
20
+ paused: true,
21
+ time: 0,
22
+ duration: NaN
23
+ };
24
+ let last = NOTHING;
25
+ function globals() {
26
+ return globalThis;
27
+ }
28
+ function notify() {
29
+ for (const fn of [...watchers]) fn();
30
+ }
31
+ /**
32
+ * The framework, once the page has it. Resolves null where the SDK never
33
+ * arrives: a browser without Cast, a page whose CSP refused the script, or
34
+ * `sdk="none"` with no script of the page's own.
35
+ */
36
+ function load(sdk) {
37
+ var _held$cast;
38
+ if (framework !== null) return framework;
39
+ const held = globals();
40
+ if (((_held$cast = held.cast) === null || _held$cast === void 0 ? void 0 : _held$cast.framework) !== void 0) {
41
+ framework = Promise.resolve(held.cast.framework);
42
+ return framework;
43
+ }
44
+ if (!("chrome" in held)) {
45
+ framework = Promise.resolve(null);
46
+ return framework;
47
+ }
48
+ framework = new Promise((resolve) => {
49
+ const previous = held.__onGCastApiAvailable;
50
+ held.__onGCastApiAvailable = (available) => {
51
+ var _globals$cast$framewo, _globals$cast;
52
+ previous === null || previous === void 0 || previous(available);
53
+ resolve(available ? (_globals$cast$framewo = (_globals$cast = globals().cast) === null || _globals$cast === void 0 ? void 0 : _globals$cast.framework) !== null && _globals$cast$framewo !== void 0 ? _globals$cast$framewo : null : null);
54
+ };
55
+ if (sdk === "none") return;
56
+ const script = document.createElement("script");
57
+ script.src = SDK_URL;
58
+ script.async = true;
59
+ script.addEventListener("error", () => resolve(null));
60
+ document.head.append(script);
61
+ });
62
+ return framework;
63
+ }
64
+ /** The context, set up once per page. */
65
+ function context(fw, receiver) {
66
+ const ctx = fw.CastContext.getInstance();
67
+ const wanted = receiver !== null && receiver !== void 0 ? receiver : DEFAULT_RECEIVER;
68
+ if (configured === null) {
69
+ var _globals$chrome$cast$, _globals$chrome;
70
+ const policy = (_globals$chrome$cast$ = (_globals$chrome = globals().chrome) === null || _globals$chrome === void 0 || (_globals$chrome = _globals$chrome.cast) === null || _globals$chrome === void 0 ? void 0 : _globals$chrome.AutoJoinPolicy.ORIGIN_SCOPED) !== null && _globals$chrome$cast$ !== void 0 ? _globals$chrome$cast$ : "origin_scoped";
71
+ ctx.setOptions({
72
+ receiverApplicationId: wanted,
73
+ autoJoinPolicy: policy
74
+ });
75
+ configured = wanted;
76
+ ctx.addEventListener(fw.CastContextEventType.CAST_STATE_CHANGED, notify);
77
+ ctx.addEventListener(fw.CastContextEventType.SESSION_STATE_CHANGED, (event) => {
78
+ var _event$sessionState;
79
+ onSession(fw, ctx, (_event$sessionState = event.sessionState) !== null && _event$sessionState !== void 0 ? _event$sessionState : "");
80
+ });
81
+ remotePlayer = new fw.RemotePlayer();
82
+ remoteController = new fw.RemotePlayerController(remotePlayer);
83
+ remoteController.addEventListener(fw.RemotePlayerEventType.ANY_CHANGE, () => {
84
+ const remote = remotePlayer;
85
+ if ((remote === null || remote === void 0 ? void 0 : remote.isConnected) === true && remote.isMediaLoaded) last = {
86
+ loaded: true,
87
+ paused: remote.isPaused,
88
+ time: remote.currentTime,
89
+ duration: remote.duration > 0 ? remote.duration : Number.POSITIVE_INFINITY
90
+ };
91
+ notify();
92
+ });
93
+ } else if (receiver !== null && receiver !== configured) console.error(`mattebox-player: the Cast context already uses receiver ${configured}`);
94
+ return ctx;
95
+ }
96
+ function onSession(fw, ctx, state) {
97
+ const states = fw.SessionState;
98
+ if (state === states.SESSION_STARTED && owner !== null && owner.pending) {
99
+ last = NOTHING;
100
+ owner.begin(ctx);
101
+ } else if (state === states.SESSION_RESUMED) {
102
+ var _ctx$getCurrentSessio;
103
+ const playing = (_ctx$getCurrentSessio = ctx.getCurrentSession()) === null || _ctx$getCurrentSessio === void 0 || (_ctx$getCurrentSessio = _ctx$getCurrentSessio.getMediaSession()) === null || _ctx$getCurrentSessio === void 0 || (_ctx$getCurrentSessio = _ctx$getCurrentSessio.media) === null || _ctx$getCurrentSessio === void 0 ? void 0 : _ctx$getCurrentSessio.contentId;
104
+ const found = [...connected].find((cast) => cast.plays(playing));
105
+ if (found !== void 0 && owner === null) {
106
+ owner = found;
107
+ found.adopt();
108
+ }
109
+ } else if (state === states.SESSION_ENDED || state === states.SESSION_START_FAILED) {
110
+ const ended = owner;
111
+ owner = null;
112
+ ended === null || ended === void 0 || ended.end(state === states.SESSION_ENDED);
113
+ }
114
+ notify();
115
+ }
116
+ /** One per player, found again by its button and its screen. Weak, so a player that leaves is not held. */
117
+ const players = /* @__PURE__ */ new WeakMap();
118
+ /** The players with a cast button attached, for a session Chrome rejoins on load. */
119
+ const connected = /* @__PURE__ */ new Set();
120
+ var CastOf = class {
121
+ constructor(player) {
122
+ this.player = player;
123
+ this.target = null;
124
+ this.ctx = null;
125
+ this.fw = null;
126
+ this.pending = false;
127
+ this.casting = false;
128
+ this.handoff = null;
129
+ }
130
+ connect(receiver, sdk) {
131
+ connected.add(this);
132
+ load(sdk).then((fw) => {
133
+ if (fw === null) return;
134
+ this.fw = fw;
135
+ this.ctx = context(fw, receiver);
136
+ notify();
137
+ });
138
+ }
139
+ disconnect() {
140
+ connected.delete(this);
141
+ }
142
+ state() {
143
+ const ctx = this.ctx;
144
+ const fw = this.fw;
145
+ if (ctx === null || fw === null) return "unavailable";
146
+ if (this.casting) return "casting";
147
+ const cast = ctx.getCastState();
148
+ if (cast === fw.CastState.NO_DEVICES_AVAILABLE) return "unavailable";
149
+ if (cast === fw.CastState.CONNECTING || this.pending) return "connecting";
150
+ return "idle";
151
+ }
152
+ device() {
153
+ var _this$ctx$getCurrentS, _this$ctx;
154
+ if (!this.casting) return null;
155
+ return (_this$ctx$getCurrentS = (_this$ctx = this.ctx) === null || _this$ctx === void 0 || (_this$ctx = _this$ctx.getCurrentSession()) === null || _this$ctx === void 0 ? void 0 : _this$ctx.getCastDevice().friendlyName) !== null && _this$ctx$getCurrentS !== void 0 ? _this$ctx$getCurrentS : null;
156
+ }
157
+ remote() {
158
+ return last;
159
+ }
160
+ start(target) {
161
+ const ctx = this.ctx;
162
+ if (ctx === null || this.casting) return;
163
+ if (owner !== null && owner !== this) {
164
+ ctx.endCurrentSession(true);
165
+ return;
166
+ }
167
+ this.target = target;
168
+ this.pending = true;
169
+ owner = this;
170
+ notify();
171
+ ctx.requestSession().catch(() => {
172
+ if (owner === this) owner = null;
173
+ this.pending = false;
174
+ notify();
175
+ });
176
+ }
177
+ stop() {
178
+ var _this$ctx2;
179
+ if (this.casting) (_this$ctx2 = this.ctx) === null || _this$ctx2 === void 0 || _this$ctx2.endCurrentSession(true);
180
+ }
181
+ playPause() {
182
+ if (this.casting) remoteController === null || remoteController === void 0 || remoteController.playOrPause();
183
+ }
184
+ seek(time) {
185
+ if (!this.casting || remotePlayer === null) return;
186
+ remotePlayer.currentTime = time;
187
+ remoteController === null || remoteController === void 0 || remoteController.seek();
188
+ }
189
+ watch(fn) {
190
+ watchers.add(fn);
191
+ return () => {
192
+ watchers.delete(fn);
193
+ };
194
+ }
195
+ /** Whether this player's session plays `url`. */
196
+ plays(url) {
197
+ var _this$player$player;
198
+ return url !== void 0 && ((_this$player$player = this.player.player) === null || _this$player$player === void 0 || (_this$player$player = _this$player$player.session) === null || _this$player$player === void 0 ? void 0 : _this$player$player.source.url) === url;
199
+ }
200
+ /** A session started for this player: freeze the picture and send the source. */
201
+ begin(ctx) {
202
+ var _this$player$player$s, _this$player$player2, _this$target, _ctx$getCurrentSessio2;
203
+ this.pending = false;
204
+ const session = (_this$player$player$s = (_this$player$player2 = this.player.player) === null || _this$player$player2 === void 0 ? void 0 : _this$player$player2.session) !== null && _this$player$player$s !== void 0 ? _this$player$player$s : null;
205
+ const video = this.player.video;
206
+ const time = video.currentTime;
207
+ const autoplay = !video.paused;
208
+ this.freeze();
209
+ if (session === null) return;
210
+ const request = this.request(session.source, time, autoplay);
211
+ if (request === null) return;
212
+ const event = new CustomEvent("castload", {
213
+ detail: request,
214
+ bubbles: true,
215
+ composed: true,
216
+ cancelable: true
217
+ });
218
+ if (((_this$target = this.target) === null || _this$target === void 0 ? void 0 : _this$target.dispatchEvent(event)) === false) return;
219
+ (_ctx$getCurrentSessio2 = ctx.getCurrentSession()) === null || _ctx$getCurrentSessio2 === void 0 || _ctx$getCurrentSessio2.loadMedia(request).catch(() => {});
220
+ }
221
+ /** A session Chrome rejoined, already playing this player's source. */
222
+ adopt() {
223
+ this.freeze();
224
+ }
225
+ freeze() {
226
+ const engine = this.player.engine;
227
+ this.handoff = { live: live(engine) };
228
+ this.player.video.pause();
229
+ engine === null || engine === void 0 || engine.suspend();
230
+ this.casting = true;
231
+ this.player.toggleAttribute("casting", true);
232
+ notify();
233
+ }
234
+ /** The session ended: give the video back where the receiver left it. */
235
+ end(cleanly) {
236
+ this.pending = false;
237
+ if (!this.casting) return;
238
+ this.casting = false;
239
+ const handoff = this.handoff;
240
+ this.handoff = null;
241
+ const video = this.player.video;
242
+ const engine = this.player.engine;
243
+ engine === null || engine === void 0 || engine.resume();
244
+ this.player.removeAttribute("casting");
245
+ if (!cleanly || !last.loaded) return;
246
+ if ((handoff === null || handoff === void 0 ? void 0 : handoff.live) !== true && Number.isFinite(last.time)) video.currentTime = last.time;
247
+ if (!last.paused) video.play().catch(() => void 0);
248
+ }
249
+ /** The load request: the source, the time, the stream type, and the video's own text tracks. */
250
+ request(source, time, autoplay) {
251
+ var _globals$chrome2, _source$type;
252
+ const media = (_globals$chrome2 = globals().chrome) === null || _globals$chrome2 === void 0 || (_globals$chrome2 = _globals$chrome2.cast) === null || _globals$chrome2 === void 0 ? void 0 : _globals$chrome2.media;
253
+ if (media === void 0) return null;
254
+ const engine = this.player.engine;
255
+ const type = (_source$type = source.type) !== null && _source$type !== void 0 ? _source$type : "";
256
+ const info = new media.MediaInfo(source.url, type);
257
+ info.streamType = live(engine) ? media.StreamType.LIVE : media.StreamType.BUFFERED;
258
+ if (/mpegurl/i.test(type)) {
259
+ const ts = tsSegments(engine);
260
+ info.hlsSegmentFormat = ts ? "ts" : "fmp4";
261
+ info.hlsVideoSegmentFormat = ts ? "mpeg2_ts" : "fmp4";
262
+ }
263
+ const tracks = [];
264
+ for (const node of this.player.video.querySelectorAll("track[src]")) {
265
+ const track = node;
266
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
267
+ const out = new media.Track(tracks.length + 1, media.TrackType.TEXT);
268
+ out.trackContentId = track.src;
269
+ out.trackContentType = "text/vtt";
270
+ out.subtype = track.kind === "captions" ? media.TextTrackType.CAPTIONS : media.TextTrackType.SUBTITLES;
271
+ out.name = track.label;
272
+ out.language = track.srclang;
273
+ tracks.push(out);
274
+ }
275
+ info.tracks = tracks.length > 0 ? tracks : null;
276
+ const request = new media.LoadRequest(info);
277
+ request.currentTime = time;
278
+ request.autoplay = autoplay;
279
+ return request;
280
+ }
281
+ };
282
+ /** Whether the engine's presentation carries MPEG-TS renditions. */
283
+ function tsSegments(engine) {
284
+ const presentation = engine === null || engine === void 0 ? void 0 : engine.stats.snapshot().presentation;
285
+ if (presentation === null || presentation === void 0) return false;
286
+ for (const period of presentation.periods) for (const track of period.tracks) for (const rendition of track.renditions) if (/mp2t/i.test(rendition.mimeType)) return true;
287
+ return false;
288
+ }
289
+ /** The player's Cast, one per player, shared by its cast button and cast screen. */
290
+ function cast(player) {
291
+ let held = players.get(player);
292
+ if (held === void 0) {
293
+ held = new CastOf(player);
294
+ players.set(player, held);
295
+ }
296
+ return held;
297
+ }
298
+ //#endregion
299
+ export { cast };
@@ -0,0 +1,84 @@
1
+ import { PLAYER, findPlayer } from "./host.js";
2
+ //#region src/component.ts
3
+ var Component = class extends HTMLElement {
4
+ constructor() {
5
+ super();
6
+ this.player = null;
7
+ this.offs = [];
8
+ this.epoch = 0;
9
+ }
10
+ connectedCallback() {
11
+ const found = findPlayer(this);
12
+ if (found === null) return;
13
+ this.epoch += 1;
14
+ const epoch = this.epoch;
15
+ const attach = () => {
16
+ if (epoch !== this.epoch || !this.isConnected) return;
17
+ customElements.upgrade(found);
18
+ if (!("video" in found)) return;
19
+ this.player = found;
20
+ this.attach(this.player);
21
+ };
22
+ if ("video" in found) attach();
23
+ else customElements.whenDefined(PLAYER).then(attach);
24
+ }
25
+ disconnectedCallback() {
26
+ this.epoch += 1;
27
+ for (const off of this.offs) off();
28
+ this.offs = [];
29
+ const player = this.player;
30
+ if (player === null) return;
31
+ this.player = null;
32
+ this.detach(player);
33
+ }
34
+ attributeChangedCallback() {
35
+ if (this.player !== null) this.render();
36
+ }
37
+ /** Adds `fn` for every name on `target`, to be removed on detach. */
38
+ listen(target, names, fn) {
39
+ for (const name of names) target.addEventListener(name, fn);
40
+ this.offs.push(() => {
41
+ for (const name of names) target.removeEventListener(name, fn);
42
+ });
43
+ }
44
+ /** Keeps `off` to run on detach. */
45
+ keep(off) {
46
+ this.offs.push(off);
47
+ }
48
+ /**
49
+ * Runs `fn` over the current engine now and on every session change,
50
+ * running what the last call returned first, so a control's engine
51
+ * subscriptions live exactly as long as the session. Detach runs it too.
52
+ */
53
+ follow(player, fn) {
54
+ let off;
55
+ const run = () => {
56
+ off === null || off === void 0 || off();
57
+ off = fn(player.engine);
58
+ };
59
+ this.listen(player, ["sourcechange"], run);
60
+ this.offs.push(() => {
61
+ off === null || off === void 0 || off();
62
+ off = void 0;
63
+ });
64
+ run();
65
+ }
66
+ /** Runs `fn` when any of `names` changes on `target`, or under it with `subtree`, until detach. */
67
+ observe(target, names, fn, subtree = false) {
68
+ const observer = new MutationObserver(fn);
69
+ observer.observe(target, {
70
+ attributes: true,
71
+ attributeFilter: [...names],
72
+ subtree
73
+ });
74
+ this.offs.push(() => {
75
+ observer.disconnect();
76
+ });
77
+ }
78
+ /** The control left its player. Listeners are already gone; undo anything else. */
79
+ detach(_player) {}
80
+ /** Draws the current state. Called on every observed attribute change while attached. */
81
+ render() {}
82
+ };
83
+ //#endregion
84
+ export { Component };
@@ -0,0 +1,19 @@
1
+ //#region src/host.ts
2
+ const PLAYER = "mattebox-player";
3
+ /** The parent, or the host of the shadow root `node` sits in, or null at the document. */
4
+ function above(node) {
5
+ const parent = node.parentNode;
6
+ if (parent !== null) return parent;
7
+ return node instanceof ShadowRoot ? node.host : null;
8
+ }
9
+ /** The nearest player above `node`, or null outside one. It may not be upgraded yet. */
10
+ function findPlayer(node) {
11
+ let current = node;
12
+ while (current !== null) {
13
+ if (current instanceof HTMLElement && current.localName === "mattebox-player") return current;
14
+ current = above(current);
15
+ }
16
+ return null;
17
+ }
18
+ //#endregion
19
+ export { PLAYER, findPlayer };
@@ -0,0 +1,23 @@
1
+ //#region src/icons.ts
2
+ /** Each glyph's subpaths joined into one `d`, drawn even-odd like the player's. */
3
+ const PATHS = {
4
+ play: "M10.6667 6.6548C10.6667 6.10764 11.2894 5.79346 11.7295 6.11862L24.377 15.4634C24.7377 15.7298 24.7377 16.2692 24.3771 16.5357L11.7295 25.8813C11.2895 26.2065 10.6667 25.8923 10.6667 25.3451L10.6667 6.6548Z",
5
+ pause: "M8.66667 6.66667C8.29848 6.66667 8 6.96514 8 7.33333V24.6667C8 25.0349 8.29848 25.3333 8.66667 25.3333H12.6667C13.0349 25.3333 13.3333 25.0349 13.3333 24.6667V7.33333C13.3333 6.96514 13.0349 6.66667 12.6667 6.66667H8.66667Z M19.3333 6.66667C18.9651 6.66667 18.6667 6.96514 18.6667 7.33333V24.6667C18.6667 25.0349 18.9651 25.3333 19.3333 25.3333H23.3333C23.7015 25.3333 24 25.0349 24 24.6667V7.33333C24 6.96514 23.7015 6.66667 23.3333 6.66667H19.3333Z",
6
+ cast: "M4 6h24v18H20v-2h6V8H6v1H4V6zM4 21a3 3 0 0 1 3 3H4zM4 15a9 9 0 0 1 9 9h-2a7 7 0 0 0-7-7zM4 10a14 14 0 0 1 14 14h-2A12 12 0 0 0 4 12z",
7
+ "cast-active": "M4 6h24v18H20A16 16 0 0 0 4 8zM4 21a3 3 0 0 1 3 3H4zM4 15a9 9 0 0 1 9 9h-2a7 7 0 0 0-7-7zM4 10a14 14 0 0 1 14 14h-2A12 12 0 0 0 4 12z"
8
+ };
9
+ const SVG = "http://www.w3.org/2000/svg";
10
+ /** A glyph, hidden from the accessibility tree: the button it sits in carries the name. */
11
+ function icon(name) {
12
+ const svg = document.createElementNS(SVG, "svg");
13
+ svg.setAttribute("viewBox", "0 0 32 32");
14
+ svg.setAttribute("aria-hidden", "true");
15
+ svg.setAttribute("part", "icon");
16
+ const path = document.createElementNS(SVG, "path");
17
+ path.setAttribute("fill-rule", "evenodd");
18
+ path.setAttribute("d", PATHS[name]);
19
+ svg.append(path);
20
+ return svg;
21
+ }
22
+ //#endregion
23
+ export { icon };
@@ -0,0 +1,17 @@
1
+ import { MbxCastButton } from "./cast-button.js";
2
+ import { MbxCastScreen } from "./cast-screen.js";
3
+ //#region src/index.ts
4
+ /**
5
+ * @mattebox/player-cast: <mbx-cast-button> and <mbx-cast-screen>, Chromecast
6
+ * from the player over Google's sender SDK. Importing this module registers
7
+ * both elements; that is the package's one side effect. It is a package of
8
+ * its own because the button loads Google's script onto the page, which the
9
+ * player never does on an integrator's behalf: the AirPlay button, over
10
+ * Safari's own API, ships with the player.
11
+ */
12
+ const CAST_BUTTON = "mbx-cast-button";
13
+ const CAST_SCREEN = "mbx-cast-screen";
14
+ if (customElements.get("mbx-cast-button") === void 0) customElements.define(CAST_BUTTON, MbxCastButton);
15
+ if (customElements.get("mbx-cast-screen") === void 0) customElements.define(CAST_SCREEN, MbxCastScreen);
16
+ //#endregion
17
+ export { CAST_BUTTON, CAST_SCREEN, MbxCastButton, MbxCastScreen };