@mattebox/player-cast 0.1.0 → 0.2.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.
- package/README.md +22 -16
- package/dist/cast-button.js +58 -64
- package/dist/cast-button.js.map +1 -1
- package/dist/cast-screen.d.ts.map +1 -1
- package/dist/cast-screen.js +113 -145
- package/dist/cast-screen.js.map +1 -1
- package/dist/cast.js +258 -311
- package/dist/cast.js.map +1 -1
- package/dist/cdn/mattebox-player-cast.min.js +1 -91
- package/dist/component.js +85 -88
- package/dist/component.js.map +1 -1
- package/dist/es2015/cast-screen.js +2 -34
- package/dist/es2015/shared.js +4 -59
- package/dist/host.js +15 -13
- package/dist/host.js.map +1 -1
- package/dist/icons.js +19 -46
- package/dist/icons.js.map +1 -1
- package/dist/index.js +17 -19
- package/dist/index.js.map +1 -1
- package/dist/shared.d.ts +2 -10
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +54 -107
- package/dist/shared.js.map +1 -1
- package/dist/slider.js +151 -160
- package/dist/slider.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
`<mbx-cast-button>` and `<mbx-cast-screen>` for the
|
|
7
7
|
[Mattebox player](https://github.com/jboix/mattebox-player): Chromecast
|
|
8
|
-
from the page, over Google's sender SDK.
|
|
9
|
-
session, and the screen covers the picture while the receiver plays, with
|
|
10
|
-
its name, play and pause, the seek slider, the time and a stop button.
|
|
11
|
-
Importing the package registers both elements.
|
|
8
|
+
from the page, over Google's sender SDK.
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
the
|
|
15
|
-
|
|
10
|
+
- The button starts and ends a session.
|
|
11
|
+
- The screen covers the picture while the receiver plays. It shows the name of the receiver, play and pause, the seek slider, the time and a stop button.
|
|
12
|
+
|
|
13
|
+
The import registers both elements.
|
|
14
|
+
|
|
15
|
+
The cast is a package of its own because the button loads Google's script
|
|
16
|
+
onto the page. The player never loads third-party code for a page. The
|
|
17
|
+
AirPlay button uses Safari's own API, so it is part of the player.
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
20
|
npm install @mattebox/player-cast @mattebox/player mattebox
|
|
@@ -35,12 +37,15 @@ npm install @mattebox/player-cast @mattebox/player mattebox
|
|
|
35
37
|
</script>
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
receiver at the video's time.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
When a session starts, the player pauses the video and suspends the engine,
|
|
41
|
+
so the engine requests nothing during the cast. It then sends the source of
|
|
42
|
+
the session to the receiver, at the video's current time. When the session
|
|
43
|
+
ends, the engine resumes and the video continues from the time where the
|
|
44
|
+
receiver stopped.
|
|
45
|
+
|
|
46
|
+
The receiver plays the URL by itself, so DRM and signed URLs are the page's
|
|
47
|
+
to arrange, in the `castload` event. The event carries the load request
|
|
48
|
+
before the button sends it:
|
|
44
49
|
|
|
45
50
|
```ts
|
|
46
51
|
button.addEventListener('castload', (event) => {
|
|
@@ -48,7 +53,8 @@ button.addEventListener('castload', (event) => {
|
|
|
48
53
|
});
|
|
49
54
|
```
|
|
50
55
|
|
|
51
|
-
`receiver` on the button names the receiver application, the
|
|
52
|
-
Receiver
|
|
56
|
+
`receiver` on the button names the receiver application. Without it, the
|
|
57
|
+
button uses the Default Media Receiver. With `sdk="none"`, the page loads
|
|
58
|
+
the SDK instead of the button. The
|
|
53
59
|
[guide](https://github.com/jboix/mattebox-player/blob/main/docs/guide/03-the-element.md#casting)
|
|
54
|
-
|
|
60
|
+
explains the rest.
|
package/dist/cast-button.js
CHANGED
|
@@ -1,65 +1,59 @@
|
|
|
1
|
-
import { cast } from
|
|
2
|
-
import { Component } from
|
|
3
|
-
import { icon } from
|
|
4
|
-
import { BUTTON_STYLE, iconSlots, show, style } from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
show(this.slots, active ? 'active' : 'default');
|
|
60
|
-
this.button.setAttribute('aria-label', active
|
|
61
|
-
? (this.getAttribute('label-stop') ?? 'Stop casting')
|
|
62
|
-
: (this.getAttribute('label-start') ?? 'Cast'));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
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
|
+
this.api?.disconnect();
|
|
41
|
+
this.api = null;
|
|
42
|
+
}
|
|
43
|
+
render() {
|
|
44
|
+
const player = this.player;
|
|
45
|
+
const api = this.api;
|
|
46
|
+
if (player === null || api === null) return;
|
|
47
|
+
const state = api.state();
|
|
48
|
+
const active = state === "casting";
|
|
49
|
+
this.hidden = state === "unavailable";
|
|
50
|
+
this.button.disabled = !active && player.player?.session == null;
|
|
51
|
+
this.button.toggleAttribute("aria-busy", state === "connecting");
|
|
52
|
+
show(this.slots, active ? "active" : "default");
|
|
53
|
+
this.button.setAttribute("aria-label", active ? this.getAttribute("label-stop") ?? "Stop casting" : this.getAttribute("label-start") ?? "Cast");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { MbxCastButton };
|
|
58
|
+
|
|
65
59
|
//# sourceMappingURL=cast-button.js.map
|
package/dist/cast-button.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-button.js","
|
|
1
|
+
{"version":3,"file":"cast-button.js","names":[],"sources":["../src/cast-button.ts"],"sourcesContent":["/**\n * <mbx-cast-button>: starts and ends a Chromecast session for the player,\n * and is hidden where there is no Cast: a browser without the SDK, or no\n * device on the network. `receiver` is the receiver application id, the\n * Default Media Receiver without one; `sdk=\"none\"` leaves loading the\n * sender SDK to the page. The glyph is `icon` or `icon-active`, the name\n * `label-start` or `label-stop`.\n *\n * Before the request goes to the receiver the button dispatches\n * `castload`, cancelable, with the request as `detail`, so a page adds\n * `customData` for its receiver or cancels. The player carries `casting`\n * while the session runs, and the cast screen drives the receiver.\n */\nimport type { Cast } from './cast.js';\nimport { cast } from './cast.js';\nimport { Component } from './component.js';\nimport type { PlayerHost } from './host.js';\nimport { icon } from './icons.js';\nimport { BUTTON_STYLE, iconSlots, show, style } from './shared.js';\n\ntype State = 'default' | 'active';\nconst STATES: readonly State[] = ['default', 'active'];\n\nexport class MbxCastButton extends Component {\n static get observedAttributes(): readonly string[] {\n return ['label-start', 'label-stop'];\n }\n\n declare private readonly button: HTMLButtonElement;\n declare private readonly slots: Record<State, HTMLSlotElement>;\n declare private api: Cast | null;\n\n constructor() {\n super();\n this.api = null;\n const root = this.attachShadow({ mode: 'open' });\n this.button = document.createElement('button');\n this.button.type = 'button';\n this.button.setAttribute('part', 'button');\n this.slots = iconSlots(STATES, (state) => icon(state === 'active' ? 'cast-active' : 'cast'));\n for (const state of STATES) this.button.append(this.slots[state]);\n this.button.addEventListener('click', () => {\n const api = this.api;\n if (api === null) return;\n if (api.state() === 'casting') api.stop();\n else api.start(this);\n });\n root.append(style(BUTTON_STYLE), this.button);\n }\n\n protected override attach(player: PlayerHost): void {\n const api = cast(player);\n this.api = api;\n api.connect(this.getAttribute('receiver'), this.getAttribute('sdk'));\n this.keep(\n api.watch(() => {\n this.render();\n }),\n );\n // A session decides whether there is anything to send.\n this.follow(player, () => {\n this.render();\n return undefined;\n });\n }\n\n protected override detach(): void {\n this.api?.disconnect();\n this.api = null;\n }\n\n protected override render(): void {\n const player = this.player;\n const api = this.api;\n if (player === null || api === null) return;\n const state = api.state();\n const active = state === 'casting';\n this.hidden = state === 'unavailable';\n // Nothing loaded means nothing to send; the receiver would show its idle screen.\n this.button.disabled = !active && player.player?.session == null;\n this.button.toggleAttribute('aria-busy', state === 'connecting');\n show(this.slots, active ? 'active' : 'default');\n this.button.setAttribute(\n 'aria-label',\n active\n ? (this.getAttribute('label-stop') ?? 'Stop casting')\n : (this.getAttribute('label-start') ?? 'Cast'),\n );\n }\n}\n"],"mappings":";;;;;AAqBA,MAAM,SAA2B,CAAC,WAAW,QAAQ;AAErD,IAAa,gBAAb,cAAmC,UAAU;CAC3C,WAAW,qBAAwC;EACjD,OAAO,CAAC,eAAe,YAAY;CACrC;CAMA,cAAc;EACZ,MAAM;EACN,KAAK,MAAM;EACX,MAAM,OAAO,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;EAC/C,KAAK,SAAS,SAAS,cAAc,QAAQ;EAC7C,KAAK,OAAO,OAAO;EACnB,KAAK,OAAO,aAAa,QAAQ,QAAQ;EACzC,KAAK,QAAQ,UAAU,SAAS,UAAU,KAAK,UAAU,WAAW,gBAAgB,MAAM,CAAC;EAC3F,KAAK,MAAM,SAAS,QAAQ,KAAK,OAAO,OAAO,KAAK,MAAM,MAAM;EAChE,KAAK,OAAO,iBAAiB,eAAe;GAC1C,MAAM,MAAM,KAAK;GACjB,IAAI,QAAQ,MAAM;GAClB,IAAI,IAAI,MAAM,MAAM,WAAW,IAAI,KAAK;QACnC,IAAI,MAAM,IAAI;EACrB,CAAC;EACD,KAAK,OAAO,MAAM,YAAY,GAAG,KAAK,MAAM;CAC9C;CAEA,OAA0B,QAA0B;EAClD,MAAM,MAAM,KAAK,MAAM;EACvB,KAAK,MAAM;EACX,IAAI,QAAQ,KAAK,aAAa,UAAU,GAAG,KAAK,aAAa,KAAK,CAAC;EACnE,KAAK,KACH,IAAI,YAAY;GACd,KAAK,OAAO;EACd,CAAC,CACH;EAEA,KAAK,OAAO,cAAc;GACxB,KAAK,OAAO;EAEd,CAAC;CACH;CAEA,SAAkC;EAChC,KAAK,KAAK,WAAW;EACrB,KAAK,MAAM;CACb;CAEA,SAAkC;EAChC,MAAM,SAAS,KAAK;EACpB,MAAM,MAAM,KAAK;EACjB,IAAI,WAAW,QAAQ,QAAQ,MAAM;EACrC,MAAM,QAAQ,IAAI,MAAM;EACxB,MAAM,SAAS,UAAU;EACzB,KAAK,SAAS,UAAU;EAExB,KAAK,OAAO,WAAW,CAAC,UAAU,OAAO,QAAQ,WAAW;EAC5D,KAAK,OAAO,gBAAgB,aAAa,UAAU,YAAY;EAC/D,KAAK,KAAK,OAAO,SAAS,WAAW,SAAS;EAC9C,KAAK,OAAO,aACV,cACA,SACK,KAAK,aAAa,YAAY,KAAK,iBACnC,KAAK,aAAa,aAAa,KAAK,MAC3C;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-screen.d.ts","sourceRoot":"","sources":["../src/cast-screen.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cast-screen.d.ts","sourceRoot":"","sources":["../src/cast-screen.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAuB5C,qBAAa,aAAc,SAAQ,SAAS;IAC1C,MAAM,KAAK,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAEjD;IAED,iBAAyB,MAAM,CAAc;IAC7C,iBAAyB,IAAI,CAAoB;IACjD,iBAAyB,KAAK,CAAiC;IAC/D,iBAAyB,IAAI,CAAS;IACtC,iBAAyB,GAAG,CAAc;IAC1C,iBAAyB,IAAI,CAAc;IAC3C,iBAAyB,IAAI,CAAoB;IACjD,QAAgB,GAAG,CAAc;IACjC,kGAAkG;IAClG,QAAgB,IAAI,CAAgB;;IA8CpC,sEAAsE;IAC7D,iBAAiB,IAAI,IAAI;cAKf,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;cAWhC,MAAM,IAAI,IAAI;IAKjC,yEAAyE;IACzE,OAAO,CAAC,KAAK;cAcM,MAAM,IAAI,IAAI;CAsBlC"}
|
package/dist/cast-screen.js
CHANGED
|
@@ -1,148 +1,116 @@
|
|
|
1
|
-
import { cast } from
|
|
2
|
-
import { Component } from
|
|
3
|
-
import { icon } from
|
|
4
|
-
import { BUTTON_STYLE, describe, el, fill, format, iconSlots,
|
|
5
|
-
import { slider } from
|
|
6
|
-
|
|
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 STYLE_CSS = ":host{color:var(--mbx-text);font:400 15px / 1.4 var(--mbx-font);text-align:center;background:#000000d9;justify-content:center;align-items:center;padding:16px;display:flex;position:absolute;top:0;bottom:0;left:0;right:0}:host([hidden]){display:none}[part~=box]{flex-direction:column;align-items:center;width:100%;max-width:420px;display:flex}[part~=box]>:not([hidden])~:not([hidden]){margin-top:12px}[part~=title]{font-size:18px;font-weight:600}[part~=row]{justify-content:center;align-items:center;width:100%;display:flex}[part~=row]>:not([hidden])~:not([hidden]){margin-left:12px}[part~=slider]{flex:1}[part~=time]{font-variant-numeric:tabular-nums;color:var(--mbx-muted);font-size:13px}[part~=stop]{height:36px;font:inherit;color:inherit;border-radius:var(--mbx-radius);cursor:pointer;background:0 0;border:1px solid;padding:0 16px;font-weight:600}[part~=stop]:focus{outline:2px solid var(--mbx-accent);outline-offset:-2px}[part~=stop]:focus:not(:focus-visible){outline:none}[hidden]{display:none}";
|
|
8
|
+
const STATES = ["play", "pause"];
|
|
7
9
|
const STEP = 5;
|
|
8
10
|
const PAGE = 30;
|
|
9
|
-
const STYLE =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return;
|
|
115
|
-
const remote = api.remote();
|
|
116
|
-
const seekable = remote.loaded && Number.isFinite(remote.duration);
|
|
117
|
-
this.seek.root.hidden = !seekable;
|
|
118
|
-
this.time.hidden = !seekable;
|
|
119
|
-
if (!seekable)
|
|
120
|
-
return;
|
|
121
|
-
const at = this.held ?? remote.time;
|
|
122
|
-
this.seek.range(0, remote.duration);
|
|
123
|
-
this.seek.set(at, describe(at, remote.duration));
|
|
124
|
-
this.time.textContent = `${format(at)} / ${format(remote.duration)}`;
|
|
125
|
-
}
|
|
126
|
-
render() {
|
|
127
|
-
const api = this.api;
|
|
128
|
-
if (api === null)
|
|
129
|
-
return;
|
|
130
|
-
const remote = api.remote();
|
|
131
|
-
// Named while hidden too: a screen reader walks the tree either way.
|
|
132
|
-
const state = remote.loaded && !remote.paused ? 'pause' : 'play';
|
|
133
|
-
show(this.slots, state);
|
|
134
|
-
this.play.setAttribute('aria-label', this.getAttribute(`label-${state}`) ?? (state === 'play' ? 'Play' : 'Pause'));
|
|
135
|
-
this.seek.label(this.getAttribute('label-seek') ?? 'Seek');
|
|
136
|
-
this.stop.textContent = this.getAttribute('label-stop') ?? 'Stop casting';
|
|
137
|
-
const casting = api.state() === 'casting';
|
|
138
|
-
this.hidden = !casting;
|
|
139
|
-
if (!casting)
|
|
140
|
-
return;
|
|
141
|
-
this.title_.textContent = fill(this.getAttribute('label') ?? 'Casting to {device}', {
|
|
142
|
-
device: api.device() ?? '',
|
|
143
|
-
});
|
|
144
|
-
this.play.disabled = !remote.loaded;
|
|
145
|
-
this.paint();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
11
|
+
const STYLE = BUTTON_STYLE + SLIDER_STYLE + STYLE_CSS;
|
|
12
|
+
var MbxCastScreen = class extends Component {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return [
|
|
15
|
+
"label",
|
|
16
|
+
"label-stop",
|
|
17
|
+
"label-play",
|
|
18
|
+
"label-pause",
|
|
19
|
+
"label-seek"
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.api = null;
|
|
25
|
+
this.held = null;
|
|
26
|
+
const root = this.attachShadow({ mode: "open" });
|
|
27
|
+
const box = el("div", "box");
|
|
28
|
+
this.title_ = el("div", "title");
|
|
29
|
+
this.play = document.createElement("button");
|
|
30
|
+
this.play.type = "button";
|
|
31
|
+
this.play.setAttribute("part", "button play");
|
|
32
|
+
this.slots = iconSlots(STATES, icon);
|
|
33
|
+
for (const state of STATES) this.play.append(this.slots[state]);
|
|
34
|
+
this.play.addEventListener("click", () => {
|
|
35
|
+
this.api?.playPause();
|
|
36
|
+
});
|
|
37
|
+
this.seek = slider({
|
|
38
|
+
step: () => STEP,
|
|
39
|
+
page: () => PAGE,
|
|
40
|
+
onInput: (value) => {
|
|
41
|
+
if (this.seek.dragging()) {
|
|
42
|
+
this.held = value;
|
|
43
|
+
this.paint();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.held = null;
|
|
47
|
+
this.api?.seek(value);
|
|
48
|
+
},
|
|
49
|
+
onDrag: (on) => {
|
|
50
|
+
this.toggleAttribute("dragging", on);
|
|
51
|
+
if (!on) this.held = null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.time = el("span", "time");
|
|
55
|
+
this.row = el("div", "row");
|
|
56
|
+
this.row.append(this.play, this.seek.root, this.time);
|
|
57
|
+
this.stop = el("button", "stop");
|
|
58
|
+
this.stop.type = "button";
|
|
59
|
+
this.stop.addEventListener("click", () => {
|
|
60
|
+
this.api?.stop();
|
|
61
|
+
});
|
|
62
|
+
box.append(this.title_, this.row, this.stop);
|
|
63
|
+
root.append(style(STYLE), box);
|
|
64
|
+
}
|
|
65
|
+
/** Hidden before attaching: a constructor must not add attributes. */
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
if (this.player === null) this.hidden = true;
|
|
68
|
+
super.connectedCallback();
|
|
69
|
+
}
|
|
70
|
+
attach(player) {
|
|
71
|
+
const api = cast(player);
|
|
72
|
+
this.api = api;
|
|
73
|
+
this.keep(api.watch(() => {
|
|
74
|
+
this.render();
|
|
75
|
+
}));
|
|
76
|
+
this.render();
|
|
77
|
+
}
|
|
78
|
+
detach() {
|
|
79
|
+
this.api = null;
|
|
80
|
+
this.hidden = true;
|
|
81
|
+
}
|
|
82
|
+
/** The slider and the time, from the receiver or from the held thumb. */
|
|
83
|
+
paint() {
|
|
84
|
+
const api = this.api;
|
|
85
|
+
if (api === null) return;
|
|
86
|
+
const remote = api.remote();
|
|
87
|
+
const seekable = remote.loaded && Number.isFinite(remote.duration);
|
|
88
|
+
this.seek.root.hidden = !seekable;
|
|
89
|
+
this.time.hidden = !seekable;
|
|
90
|
+
if (!seekable) return;
|
|
91
|
+
const at = this.held ?? remote.time;
|
|
92
|
+
this.seek.range(0, remote.duration);
|
|
93
|
+
this.seek.set(at, describe(at, remote.duration));
|
|
94
|
+
this.time.textContent = `${format(at)} / ${format(remote.duration)}`;
|
|
95
|
+
}
|
|
96
|
+
render() {
|
|
97
|
+
const api = this.api;
|
|
98
|
+
if (api === null) return;
|
|
99
|
+
const remote = api.remote();
|
|
100
|
+
const state = remote.loaded && !remote.paused ? "pause" : "play";
|
|
101
|
+
show(this.slots, state);
|
|
102
|
+
this.play.setAttribute("aria-label", this.getAttribute(`label-${state}`) ?? (state === "play" ? "Play" : "Pause"));
|
|
103
|
+
this.seek.label(this.getAttribute("label-seek") ?? "Seek");
|
|
104
|
+
this.stop.textContent = this.getAttribute("label-stop") ?? "Stop casting";
|
|
105
|
+
const casting = api.state() === "casting";
|
|
106
|
+
this.hidden = !casting;
|
|
107
|
+
if (!casting) return;
|
|
108
|
+
this.title_.textContent = fill(this.getAttribute("label") ?? "Casting to {device}", { device: api.device() ?? "" });
|
|
109
|
+
this.play.disabled = !remote.loaded;
|
|
110
|
+
this.paint();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { MbxCastScreen };
|
|
115
|
+
|
|
148
116
|
//# sourceMappingURL=cast-screen.js.map
|
package/dist/cast-screen.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-screen.js","
|
|
1
|
+
{"version":3,"file":"cast-screen.js","names":[],"sources":["../src/cast-screen.ts"],"sourcesContent":["/**\n * <mbx-cast-screen>: covers the picture while the player casts, with the\n * receiver's name, play and pause, the seek slider, the time, and a stop\n * button. It drives the receiver through the SDK's remote player; the\n * video underneath stays paused and untouched (rule 3). It sits in the\n * player beside the video, hidden until the player carries `casting`.\n *\n * The title comes from `label` with `{device}`, the stop button's text from\n * `label-stop`, the play button's name from `label-play` and `label-pause`\n * and its glyph from `icon-play` and `icon-pause`, the slider's name from\n * `label-seek`. A live stream has no duration on the receiver, so the\n * slider and the time hide for it.\n */\nimport type { Cast } from './cast.js';\nimport { cast } from './cast.js';\nimport STYLE_CSS from './cast-screen.css?inline';\nimport { Component } from './component.js';\nimport type { PlayerHost } from './host.js';\nimport { icon } from './icons.js';\nimport {\n BUTTON_STYLE,\n describe,\n el,\n fill,\n format,\n iconSlots,\n SLIDER_STYLE,\n show,\n style,\n} from './shared.js';\nimport type { Slider } from './slider.js';\nimport { slider } from './slider.js';\n\ntype State = 'play' | 'pause';\nconst STATES: readonly State[] = ['play', 'pause'];\nconst STEP = 5;\nconst PAGE = 30;\n\nconst STYLE = BUTTON_STYLE + SLIDER_STYLE + STYLE_CSS;\n\nexport class MbxCastScreen extends Component {\n static get observedAttributes(): readonly string[] {\n return ['label', 'label-stop', 'label-play', 'label-pause', 'label-seek'];\n }\n\n declare private readonly title_: HTMLElement;\n declare private readonly play: HTMLButtonElement;\n declare private readonly slots: Record<State, HTMLSlotElement>;\n declare private readonly seek: Slider;\n declare private readonly row: HTMLElement;\n declare private readonly time: HTMLElement;\n declare private readonly stop: HTMLButtonElement;\n declare private api: Cast | null;\n /** Where the thumb is while a pointer holds it, so the receiver's reports do not move it back. */\n declare private held: number | null;\n\n constructor() {\n super();\n this.api = null;\n this.held = null;\n const root = this.attachShadow({ mode: 'open' });\n const box = el('div', 'box');\n this.title_ = el('div', 'title');\n this.play = document.createElement('button');\n this.play.type = 'button';\n this.play.setAttribute('part', 'button play');\n this.slots = iconSlots(STATES, icon);\n for (const state of STATES) this.play.append(this.slots[state]);\n this.play.addEventListener('click', () => {\n this.api?.playPause();\n });\n this.seek = slider({\n step: () => STEP,\n page: () => PAGE,\n onInput: (value) => {\n if (this.seek.dragging()) {\n this.held = value;\n this.paint();\n return;\n }\n this.held = null;\n this.api?.seek(value);\n },\n onDrag: (on) => {\n this.toggleAttribute('dragging', on);\n if (!on) this.held = null;\n },\n });\n this.time = el('span', 'time');\n this.row = el('div', 'row');\n this.row.append(this.play, this.seek.root, this.time);\n this.stop = el('button', 'stop');\n this.stop.type = 'button';\n this.stop.addEventListener('click', () => {\n this.api?.stop();\n });\n box.append(this.title_, this.row, this.stop);\n root.append(style(STYLE), box);\n }\n\n /** Hidden before attaching: a constructor must not add attributes. */\n override connectedCallback(): void {\n if (this.player === null) this.hidden = true;\n super.connectedCallback();\n }\n\n protected override attach(player: PlayerHost): void {\n const api = cast(player);\n this.api = api;\n this.keep(\n api.watch(() => {\n this.render();\n }),\n );\n this.render();\n }\n\n protected override detach(): void {\n this.api = null;\n this.hidden = true;\n }\n\n /** The slider and the time, from the receiver or from the held thumb. */\n private paint(): void {\n const api = this.api;\n if (api === null) return;\n const remote = api.remote();\n const seekable = remote.loaded && Number.isFinite(remote.duration);\n this.seek.root.hidden = !seekable;\n this.time.hidden = !seekable;\n if (!seekable) return;\n const at = this.held ?? remote.time;\n this.seek.range(0, remote.duration);\n this.seek.set(at, describe(at, remote.duration));\n this.time.textContent = `${format(at)} / ${format(remote.duration)}`;\n }\n\n protected override render(): void {\n const api = this.api;\n if (api === null) return;\n const remote = api.remote();\n // Named while hidden too: a screen reader walks the tree either way.\n const state: State = remote.loaded && !remote.paused ? 'pause' : 'play';\n show(this.slots, state);\n this.play.setAttribute(\n 'aria-label',\n this.getAttribute(`label-${state}`) ?? (state === 'play' ? 'Play' : 'Pause'),\n );\n this.seek.label(this.getAttribute('label-seek') ?? 'Seek');\n this.stop.textContent = this.getAttribute('label-stop') ?? 'Stop casting';\n const casting = api.state() === 'casting';\n this.hidden = !casting;\n if (!casting) return;\n this.title_.textContent = fill(this.getAttribute('label') ?? 'Casting to {device}', {\n device: api.device() ?? '',\n });\n this.play.disabled = !remote.loaded;\n this.paint();\n }\n}\n"],"mappings":"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"}
|