Skip to main content

host_extensions/first_party/
media.rs

1//! Media extension — camera/audio capture and P2P media sessions.
2//!
3//! Registers as `window.host.ext.media` in the SPA WebView.
4//! Uses the core `__hostCall` bridge (shared pending-promise map).
5
6use crate::HostExtension;
7
8/// JS injection script for the media extension.
9///
10/// Reuses the core bridge via `window.__hostCall('hostBridge', ...)`.
11/// Push events (mediaTrackReady, mediaTrackStopped, mediaConnected,
12/// mediaRemoteTrack, mediaClosed, mediaError, mediaIncomingCall,
13/// mediaSignalingProgress)
14/// are dispatched through the core `window.host.on()` / `__hostPush` path.
15const HOST_EXT_MEDIA_SCRIPT: &str = r#"
16(function(){
17    if (!window.host || !window.host.ext) return;
18    window.host.ext.media = Object.freeze({
19        getUserMedia: function(constraints) {
20            return window.__hostCall('hostBridge', 'mediaGetUserMedia', {
21                audio: !!(constraints && constraints.audio),
22                video: !!(constraints && constraints.video)
23            });
24        },
25        connect: function(peer, trackIds) {
26            return window.__hostCall('hostBridge', 'mediaConnect', { peer: peer, trackIds: trackIds || [] });
27        },
28        accept: function(peer) {
29            return window.__hostCall('hostBridge', 'mediaAccept', peer ? { peer: peer } : {});
30        },
31        reject: function(peer) {
32            return window.__hostCall('hostBridge', 'mediaReject', peer ? { peer: peer } : {});
33        },
34        close: function(sessionId) {
35            return window.__hostCall('hostBridge', 'mediaClose', { sessionId: sessionId });
36        },
37        attachTrack: function(trackId, elementId) {
38            return window.__hostCall('hostBridge', 'mediaAttachTrack', { trackId: trackId, elementId: elementId });
39        },
40        getPeerId: function() {
41            return window.__hostCall('hostBridge', 'mediaGetPeerId', {});
42        },
43        startListening: function(address) {
44            return window.__hostCall('hostBridge', 'mediaStartListening', { address: address });
45        },
46        setTrackEnabled: function(trackId, enabled) {
47            return window.__hostCall('hostBridge', 'mediaSetTrackEnabled', { trackId: trackId, enabled: enabled });
48        },
49        groupConnect: function(peers, trackIds) {
50            return window.__hostCall('hostBridge', 'mediaGroupConnect', {
51                peers: peers || [],
52                trackIds: trackIds || []
53            });
54        },
55        groupAccept: function(peer, trackIds) {
56            return window.__hostCall('hostBridge', 'mediaGroupAccept', {
57                peer: peer,
58                trackIds: trackIds || []
59            });
60        },
61        getDisplayMedia: function() {
62            return window.__hostCall('hostBridge', 'mediaGetDisplayMedia', {});
63        },
64        addTrack: function(sessionId, trackId) {
65            return window.__hostCall('hostBridge', 'mediaAddTrack', {
66                sessionId: sessionId,
67                trackId: trackId
68            });
69        },
70        removeTrack: function(sessionId, trackId) {
71            return window.__hostCall('hostBridge', 'mediaRemoveTrack', {
72                sessionId: sessionId,
73                trackId: trackId
74            });
75        }
76    });
77})();
78"#;
79
80pub struct MediaExtension;
81
82impl Default for MediaExtension {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl MediaExtension {
89    pub fn new() -> Self {
90        Self
91    }
92}
93
94impl HostExtension for MediaExtension {
95    fn namespace(&self) -> &str {
96        "media"
97    }
98
99    fn channel(&self) -> &str {
100        // Reuses the core hostBridge channel — no separate handler needed.
101        "hostBridge"
102    }
103
104    fn inject_script(&self) -> &str {
105        HOST_EXT_MEDIA_SCRIPT
106    }
107
108    fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
109        // Messages are handled by the core js_bridge dispatcher —
110        // this extension only contributes the JS injection.
111        None
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn media_extension_basics() {
121        let ext = MediaExtension::new();
122        assert_eq!(ext.namespace(), "media");
123        assert!(ext.inject_script().contains("window.host.ext.media"));
124        assert!(ext.inject_script().contains("mediaGetUserMedia"));
125        assert!(ext.inject_script().contains("mediaConnect"));
126        assert!(ext.inject_script().contains("mediaGetDisplayMedia"));
127        assert!(ext.inject_script().contains("mediaAddTrack"));
128        assert!(ext.inject_script().contains("mediaRemoveTrack"));
129    }
130}