@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 +148 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +122 -0
- package/package.json +55 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|