@scarlett-player/airplay 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ airplayPlugin: () => airplayPlugin,
24
+ default: () => index_default,
25
+ isAirPlaySupported: () => isAirPlaySupported
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ function isAirPlaySupported() {
29
+ return typeof HTMLVideoElement !== "undefined" && typeof HTMLVideoElement.prototype.webkitShowPlaybackTargetPicker === "function";
30
+ }
31
+ function airplayPlugin() {
32
+ let api;
33
+ let video = null;
34
+ let unsubMediaLoaded = null;
35
+ const handleAvailabilityChange = (e) => {
36
+ const event = e;
37
+ const available = event.availability === "available";
38
+ api.setState("airplayAvailable", available);
39
+ api.emit(available ? "airplay:available" : "airplay:unavailable", void 0);
40
+ api.logger.debug("AirPlay availability changed", { available });
41
+ };
42
+ const handleTargetChange = () => {
43
+ const active = video?.webkitCurrentPlaybackTargetIsWireless === true;
44
+ api.setState("airplayActive", active);
45
+ api.emit(active ? "airplay:connected" : "airplay:disconnected", void 0);
46
+ };
47
+ const attachToVideo = () => {
48
+ if (video) return;
49
+ video = api.container.querySelector("video");
50
+ if (!video) {
51
+ api.logger.debug("AirPlay: No video element yet");
52
+ return;
53
+ }
54
+ api.logger.debug("AirPlay: Attaching to video element");
55
+ video.addEventListener(
56
+ "webkitplaybacktargetavailabilitychanged",
57
+ handleAvailabilityChange
58
+ );
59
+ video.addEventListener(
60
+ "webkitcurrentplaybacktargetiswirelesschanged",
61
+ handleTargetChange
62
+ );
63
+ if ("remote" in video && video.remote) {
64
+ api.logger.debug("AirPlay: RemotePlayback API available");
65
+ video.remote.watchAvailability((available) => {
66
+ api.logger.debug("AirPlay: RemotePlayback availability", { available });
67
+ if (available) {
68
+ api.setState("airplayAvailable", true);
69
+ api.emit("airplay:available", void 0);
70
+ }
71
+ }).catch((err) => {
72
+ api.logger.debug("AirPlay: RemotePlayback watchAvailability not supported", { error: err.message });
73
+ });
74
+ }
75
+ };
76
+ return {
77
+ id: "airplay",
78
+ name: "AirPlay",
79
+ type: "feature",
80
+ version: "1.0.0",
81
+ async init(pluginApi) {
82
+ api = pluginApi;
83
+ api.setState("airplayAvailable", false);
84
+ api.setState("airplayActive", false);
85
+ if (!isAirPlaySupported()) {
86
+ api.logger.debug("AirPlay not supported in this browser");
87
+ return;
88
+ }
89
+ attachToVideo();
90
+ unsubMediaLoaded = api.on("media:loaded", () => {
91
+ attachToVideo();
92
+ });
93
+ api.logger.debug("AirPlay plugin initialized");
94
+ },
95
+ async destroy() {
96
+ unsubMediaLoaded?.();
97
+ unsubMediaLoaded = null;
98
+ if (video && isAirPlaySupported()) {
99
+ video.removeEventListener(
100
+ "webkitplaybacktargetavailabilitychanged",
101
+ handleAvailabilityChange
102
+ );
103
+ video.removeEventListener(
104
+ "webkitcurrentplaybacktargetiswirelesschanged",
105
+ handleTargetChange
106
+ );
107
+ }
108
+ video = null;
109
+ api.logger.debug("AirPlay plugin destroyed");
110
+ },
111
+ async showPicker() {
112
+ if (!isAirPlaySupported()) {
113
+ api?.logger.warn("AirPlay not supported in this browser");
114
+ return;
115
+ }
116
+ if (!video) {
117
+ attachToVideo();
118
+ }
119
+ if (!video) {
120
+ api?.logger.warn("Cannot show AirPlay picker: no video element");
121
+ return;
122
+ }
123
+ const hlsPlugin = api?.getPlugin("hls-provider");
124
+ if (hlsPlugin && !hlsPlugin.isNativeHLS()) {
125
+ api?.logger.info("Switching to native HLS for AirPlay compatibility");
126
+ await hlsPlugin.switchToNative();
127
+ video = null;
128
+ attachToVideo();
129
+ }
130
+ video?.webkitShowPlaybackTargetPicker?.();
131
+ },
132
+ isAvailable() {
133
+ return api?.getState("airplayAvailable") === true;
134
+ },
135
+ isActive() {
136
+ return api?.getState("airplayActive") === true;
137
+ },
138
+ stop() {
139
+ api?.logger.debug("AirPlay stop requested (use system controls to disconnect)");
140
+ }
141
+ };
142
+ }
143
+ var index_default = airplayPlugin;
144
+ // Annotate the CommonJS export names for ESM import in node:
145
+ 0 && (module.exports = {
146
+ airplayPlugin,
147
+ isAirPlaySupported
148
+ });
@@ -0,0 +1,62 @@
1
+ import { Plugin } from '@scarlett-player/core';
2
+
3
+ /**
4
+ * AirPlay Plugin Types
5
+ */
6
+
7
+ /** AirPlay plugin interface */
8
+ interface IAirPlayPlugin extends Plugin {
9
+ readonly id: 'airplay';
10
+ /** Show the AirPlay device picker (Safari only) */
11
+ showPicker(): void;
12
+ /** Check if AirPlay is available (Safari + devices found) */
13
+ isAvailable(): boolean;
14
+ /** Check if currently casting via AirPlay */
15
+ isActive(): boolean;
16
+ /** Stop AirPlay casting */
17
+ stop(): void;
18
+ }
19
+ /** AirPlay availability event */
20
+ interface AirPlayAvailabilityEvent {
21
+ available: boolean;
22
+ }
23
+ /** AirPlay connection event */
24
+ interface AirPlayConnectionEvent {
25
+ connected: boolean;
26
+ }
27
+
28
+ /**
29
+ * AirPlay Plugin for Scarlett Player
30
+ *
31
+ * Wraps Safari's native AirPlay APIs for wireless playback.
32
+ * Gracefully degrades to no-op on non-Safari browsers.
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+
37
+ /**
38
+ * Check if AirPlay is supported (Safari only).
39
+ */
40
+ declare function isAirPlaySupported(): boolean;
41
+ /**
42
+ * Create an AirPlay plugin instance.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { airplayPlugin } from '@scarlett-player/airplay';
47
+ *
48
+ * const player = new ScarlettPlayer({
49
+ * container: '#player',
50
+ * plugins: [airplayPlugin()],
51
+ * });
52
+ *
53
+ * // Show AirPlay picker
54
+ * const airplay = player.getPlugin<IAirPlayPlugin>('airplay');
55
+ * if (airplay?.isAvailable()) {
56
+ * airplay.showPicker();
57
+ * }
58
+ * ```
59
+ */
60
+ declare function airplayPlugin(): IAirPlayPlugin;
61
+
62
+ export { type AirPlayAvailabilityEvent, type AirPlayConnectionEvent, type IAirPlayPlugin, airplayPlugin, airplayPlugin as default, isAirPlaySupported };
@@ -0,0 +1,62 @@
1
+ import { Plugin } from '@scarlett-player/core';
2
+
3
+ /**
4
+ * AirPlay Plugin Types
5
+ */
6
+
7
+ /** AirPlay plugin interface */
8
+ interface IAirPlayPlugin extends Plugin {
9
+ readonly id: 'airplay';
10
+ /** Show the AirPlay device picker (Safari only) */
11
+ showPicker(): void;
12
+ /** Check if AirPlay is available (Safari + devices found) */
13
+ isAvailable(): boolean;
14
+ /** Check if currently casting via AirPlay */
15
+ isActive(): boolean;
16
+ /** Stop AirPlay casting */
17
+ stop(): void;
18
+ }
19
+ /** AirPlay availability event */
20
+ interface AirPlayAvailabilityEvent {
21
+ available: boolean;
22
+ }
23
+ /** AirPlay connection event */
24
+ interface AirPlayConnectionEvent {
25
+ connected: boolean;
26
+ }
27
+
28
+ /**
29
+ * AirPlay Plugin for Scarlett Player
30
+ *
31
+ * Wraps Safari's native AirPlay APIs for wireless playback.
32
+ * Gracefully degrades to no-op on non-Safari browsers.
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+
37
+ /**
38
+ * Check if AirPlay is supported (Safari only).
39
+ */
40
+ declare function isAirPlaySupported(): boolean;
41
+ /**
42
+ * Create an AirPlay plugin instance.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { airplayPlugin } from '@scarlett-player/airplay';
47
+ *
48
+ * const player = new ScarlettPlayer({
49
+ * container: '#player',
50
+ * plugins: [airplayPlugin()],
51
+ * });
52
+ *
53
+ * // Show AirPlay picker
54
+ * const airplay = player.getPlugin<IAirPlayPlugin>('airplay');
55
+ * if (airplay?.isAvailable()) {
56
+ * airplay.showPicker();
57
+ * }
58
+ * ```
59
+ */
60
+ declare function airplayPlugin(): IAirPlayPlugin;
61
+
62
+ export { type AirPlayAvailabilityEvent, type AirPlayConnectionEvent, type IAirPlayPlugin, airplayPlugin, airplayPlugin as default, isAirPlaySupported };
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ // src/index.ts
2
+ function isAirPlaySupported() {
3
+ return typeof HTMLVideoElement !== "undefined" && typeof HTMLVideoElement.prototype.webkitShowPlaybackTargetPicker === "function";
4
+ }
5
+ function airplayPlugin() {
6
+ let api;
7
+ let video = null;
8
+ let unsubMediaLoaded = null;
9
+ const handleAvailabilityChange = (e) => {
10
+ const event = e;
11
+ const available = event.availability === "available";
12
+ api.setState("airplayAvailable", available);
13
+ api.emit(available ? "airplay:available" : "airplay:unavailable", void 0);
14
+ api.logger.debug("AirPlay availability changed", { available });
15
+ };
16
+ const handleTargetChange = () => {
17
+ const active = video?.webkitCurrentPlaybackTargetIsWireless === true;
18
+ api.setState("airplayActive", active);
19
+ api.emit(active ? "airplay:connected" : "airplay:disconnected", void 0);
20
+ };
21
+ const attachToVideo = () => {
22
+ if (video) return;
23
+ video = api.container.querySelector("video");
24
+ if (!video) {
25
+ api.logger.debug("AirPlay: No video element yet");
26
+ return;
27
+ }
28
+ api.logger.debug("AirPlay: Attaching to video element");
29
+ video.addEventListener(
30
+ "webkitplaybacktargetavailabilitychanged",
31
+ handleAvailabilityChange
32
+ );
33
+ video.addEventListener(
34
+ "webkitcurrentplaybacktargetiswirelesschanged",
35
+ handleTargetChange
36
+ );
37
+ if ("remote" in video && video.remote) {
38
+ api.logger.debug("AirPlay: RemotePlayback API available");
39
+ video.remote.watchAvailability((available) => {
40
+ api.logger.debug("AirPlay: RemotePlayback availability", { available });
41
+ if (available) {
42
+ api.setState("airplayAvailable", true);
43
+ api.emit("airplay:available", void 0);
44
+ }
45
+ }).catch((err) => {
46
+ api.logger.debug("AirPlay: RemotePlayback watchAvailability not supported", { error: err.message });
47
+ });
48
+ }
49
+ };
50
+ return {
51
+ id: "airplay",
52
+ name: "AirPlay",
53
+ type: "feature",
54
+ version: "1.0.0",
55
+ async init(pluginApi) {
56
+ api = pluginApi;
57
+ api.setState("airplayAvailable", false);
58
+ api.setState("airplayActive", false);
59
+ if (!isAirPlaySupported()) {
60
+ api.logger.debug("AirPlay not supported in this browser");
61
+ return;
62
+ }
63
+ attachToVideo();
64
+ unsubMediaLoaded = api.on("media:loaded", () => {
65
+ attachToVideo();
66
+ });
67
+ api.logger.debug("AirPlay plugin initialized");
68
+ },
69
+ async destroy() {
70
+ unsubMediaLoaded?.();
71
+ unsubMediaLoaded = null;
72
+ if (video && isAirPlaySupported()) {
73
+ video.removeEventListener(
74
+ "webkitplaybacktargetavailabilitychanged",
75
+ handleAvailabilityChange
76
+ );
77
+ video.removeEventListener(
78
+ "webkitcurrentplaybacktargetiswirelesschanged",
79
+ handleTargetChange
80
+ );
81
+ }
82
+ video = null;
83
+ api.logger.debug("AirPlay plugin destroyed");
84
+ },
85
+ async showPicker() {
86
+ if (!isAirPlaySupported()) {
87
+ api?.logger.warn("AirPlay not supported in this browser");
88
+ return;
89
+ }
90
+ if (!video) {
91
+ attachToVideo();
92
+ }
93
+ if (!video) {
94
+ api?.logger.warn("Cannot show AirPlay picker: no video element");
95
+ return;
96
+ }
97
+ const hlsPlugin = api?.getPlugin("hls-provider");
98
+ if (hlsPlugin && !hlsPlugin.isNativeHLS()) {
99
+ api?.logger.info("Switching to native HLS for AirPlay compatibility");
100
+ await hlsPlugin.switchToNative();
101
+ video = null;
102
+ attachToVideo();
103
+ }
104
+ video?.webkitShowPlaybackTargetPicker?.();
105
+ },
106
+ isAvailable() {
107
+ return api?.getState("airplayAvailable") === true;
108
+ },
109
+ isActive() {
110
+ return api?.getState("airplayActive") === true;
111
+ },
112
+ stop() {
113
+ api?.logger.debug("AirPlay stop requested (use system controls to disconnect)");
114
+ }
115
+ };
116
+ }
117
+ var index_default = airplayPlugin;
118
+ export {
119
+ airplayPlugin,
120
+ index_default as default,
121
+ isAirPlaySupported
122
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@scarlett-player/airplay",
3
+ "version": "0.1.0",
4
+ "description": "AirPlay Plugin for Scarlett Player",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm,cjs --dts",
21
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
22
+ "test": "vitest --run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest --coverage"
25
+ },
26
+ "peerDependencies": {
27
+ "@scarlett-player/core": "^0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@scarlett-player/core": "file:../../core",
31
+ "typescript": "^5.3.0",
32
+ "tsup": "^8.0.0",
33
+ "vitest": "^1.6.0",
34
+ "@vitest/coverage-v8": "^1.6.0",
35
+ "jsdom": "^24.0.0"
36
+ },
37
+ "keywords": [
38
+ "video",
39
+ "player",
40
+ "airplay",
41
+ "casting",
42
+ "scarlett"
43
+ ],
44
+ "author": "The Stream Platform",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/Hackney-Enterprises-Inc/scarlett-player.git",
49
+ "directory": "packages/plugins/airplay"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/Hackney-Enterprises-Inc/scarlett-player/issues"
53
+ },
54
+ "homepage": "https://github.com/Hackney-Enterprises-Inc/scarlett-player#readme"
55
+ }