├── LICENSE
├── README.md
├── app.js
├── extension
├── background.js
├── content-script.js
├── icon.png
└── manifest.json
├── images
├── extensions.png
└── preview.gif
└── index.html
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Philipp Weissensteiner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Notes
2 |
3 | ### You might not need this anymore!
4 |
5 | Recent Chrome versions (70 and up) [introduced
6 | `getDisplayMedia`](https://groups.google.com/forum/?__s=seqnqtrfky2js3hsisxx#!msg/discuss-webrtc/Uf0SrR4uxzk/uO8sLrWuEAAJ)
7 | which essentially nullify this project and the need for an extension. You can
8 | simply acquire a screen stream with something like this:
9 |
10 | ```js
11 | navigator.mediaDevices.getDisplayMedia({ audio: false, video: true })
12 | .then(stream => video.srcObject = stream)
13 | .catch(handleError);
14 | ```
15 |
16 | A working demo can be found
17 | [here](https://cdn.rawgit.com/uysalere/js-demos/master/getdisplaymedia.html).
18 |
19 |
20 | *This project has been merged into https://github.com/GoogleChrome/webrtc in
21 | case something doesn't work have [a look over
22 | there.](https://github.com/webrtc/samples/tree/master/src/content/extensions/desktopcapture)*
23 |
24 | ## Introduction
25 |
26 | *This demo app shows you how to use a Chrome extension to access the
27 | `desktopCapture` API in your web-application.*
28 |
29 | (If you're writing a WebRTC app with screen sharing, and want to avoid sending
30 | users to `chrome://flags`)
31 |
32 |
33 |
34 | ## Index
35 |
36 | - [Setup](#setup)
37 | - [How does it work?](#how)
38 | - [Application (our web-app)](#app)
39 | - [Extension](#extension)
40 | - [Glueing it together](#glue)
41 | - [Avoiding a reload after installation](#reload)
42 | - [Credits](#credits)
43 |
44 |
45 | # Setup
46 |
47 | For the Demo to work, you will need to install the extension
48 |
49 | 1. Go to [chrome://extensions]()
50 | 2. Check "Developer mode"
51 | 3. Click "Load unpacked extension..."
52 | 4. In the dialog choose the `extension` folder from the repository
53 |
54 | You should see:
55 |
56 | NOTE: your ID will differ, that's fine though.
57 |
58 |
59 | # How does it work?
60 |
61 |
62 | ## Application (our web-app)
63 |
64 | The `index.html` file contains a "Share screen" button, an empty `` tag
65 | and loads some javascript (`app.js`). Think of these two files as our
66 | "application".
67 |
68 |
69 | ## Extension
70 |
71 | The extension consists of 4 files:
72 |
73 | 1. background.js
74 | 2. content-script.js
75 | 3. manifest.json
76 | 4. icon.png // not important
77 |
78 | ### background.js
79 |
80 | > holds the main logic of the extension
81 |
82 | or in our case, has access to the [desktopCapture
83 | API](https://developer.chrome.com/extensions/desktopCapture). We get access to
84 | this API when we ask for permission in `manifest.json`:
85 |
86 | "permissions": [
87 | "desktopCapture"
88 | ]
89 |
90 | The background page ("background.js" - chrome generates the related html for us)
91 | runs in the extension process and is therefore isolated from our application
92 | environment. Meaning that we don't have a direct way to talk to our application.
93 | That's why we have the content-script.
94 |
95 | [1](https://developer.chrome.com/extensions/background_pages)
96 |
97 | ### content-script.js
98 |
99 | > If your extension needs to interact with web pages, then it needs a content
100 | > script. A content script is some JavaScript that executes in the context of a
101 | > page that's been loaded into the browser.
102 |
103 | [2](https://developer.chrome.com/extensions/overview#contentScripts)
104 |
105 | The content-script does not have access to variables or functions defined on our
106 | page, but it **has access to the DOM**.
107 |
108 |
109 | ## Glueing it together
110 |
111 |
112 | In order to call `navigator.mediaDevices.getUserMedia` in **app.js**, we need a
113 | `chromeMediaSourceId` which we get from our **background page**.
114 |
115 | We have to pass messages through the chain below (left to right):
116 |
117 | app.js | |content-script.js | |background.js | desktopCapture API
118 | ------------------| |------------------| |--------------------|
119 | window.postMessage|------->|port.postMessage |----->|port.onMessage------+
120 | | window | | port | get|*streamID*
121 | getUserMedia |<------ |window.postMessage|<-----|port.postMessage<---+
122 |
123 | Lets run through the chain:
124 |
125 | When the user clicks on "Share Screen", we post a message to **window**,
126 | because...
127 |
128 | window.postMessage({ type: 'SS_UI_REQUEST', text: 'start' }, '*');
129 |
130 | the **content-script has access to the DOM.**
131 |
132 | window.addEventListener('message', event => {
133 | if (event.data.type === 'SS_UI_REQUEST') {
134 | port.postMessage(event.data);
135 | }
136 | }, false);
137 |
138 | the content-script can also **talk to the background page**
139 |
140 | var port = chrome.runtime.connect(chrome.runtime.id);
141 |
142 | the **background page is listening on that port**,
143 |
144 | port.onMessage.addListener(function (msg) {
145 | if(msg.type === 'SS_UI_REQUEST') {
146 | requestScreenSharing(port, msg);
147 | }
148 |
149 | gets access to the stream, and **sends a message containing the
150 | chromeMediaSourceId (`streamID`) back to the port** (the content-script)
151 |
152 | function requestScreenSharing(port, msg) {
153 | desktopMediaRequestId =
154 | chrome.desktopCapture.chooseDesktopMedia(data_sources, port.sender.tab,
155 | streamId => {
156 | msg.type = 'SS_DIALOG_SUCCESS';
157 | msg.streamId = streamId;
158 | port.postMessage(msg);
159 | });
160 | }
161 |
162 | the content-script posts it back to app.js
163 |
164 | port.onMessage.addListener(msg => {
165 | window.postMessage(msg, '*');
166 | });
167 |
168 | where we finally call `navigator.mediaDevices.getUserMedia` with the `streamID`
169 |
170 | if (event.data.type && (event.data.type === 'SS_DIALOG_SUCCESS')) {
171 | startScreenStreamFrom(event.data.streamId);
172 | }
173 |
174 | function startScreenStreamFrom(streamId) {
175 | navigator.mediaDevices
176 | .getUserMedia({
177 | audio: false,
178 | video: {
179 | mandatory: {
180 | chromeMediaSource: 'desktop',
181 | chromeMediaSourceId: streamId
182 | }
183 | }
184 | })
185 | .then(stream => {
186 | videoElement = document.getElementById('video');
187 | videoElement.srcObject = stream;
188 | })
189 |
190 | *Please note that the code examples in this README are edited for brevity,
191 | complete code is in the corresponding files.*
192 |
193 |
194 | # Avoiding a reload after installation
195 |
196 | *This part was inspired by
197 | [fippo from &yet](https://blog.andyet.com/2015/03/30/talky-first-time-ux-matters).*
198 |
199 | When the extension is installed, the content-script will not be injected automatically by Chrome.
200 | The good news is that `background.js` will be exectued and we can use it to manually inject the
201 | `content-script.js` in our open pages (tabs):
202 |
203 | The relevant code can be found in `background.js` and looks something like:
204 |
205 | chrome.tabs.executeScript(currentTab.id, { file: 'js/content-script.js' }, () => {
206 | console.log('Injected content-script.');
207 | });
208 |
209 | **Please note: For this to work, you have to adjust `manifest.json` by adding `"tabs"` to the permissions section.**
210 |
211 |
212 | # Credits
213 |
214 | Thanks to the guys and gals at [&yet](http://andyet.com/) for [talky.io]()
215 |
216 | # License
217 |
218 | [MIT](http://opensource.org/licenses/MIT)
219 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | let extensionInstalled = false;
2 |
3 | document.getElementById('start').addEventListener('click', () => {
4 | // send screen-sharer request to content-script
5 | if (!extensionInstalled) {
6 | alert(
7 | 'Please install the extension:\n' +
8 | '1. Go to chrome://extensions\n' +
9 | '2. Check: "Enable Developer mode"\n' +
10 | '3. Click: "Load the unpacked extension..."\n' +
11 | '4. Choose "extension" folder from the repository\n' +
12 | '5. Reload this page'
13 | );
14 | }
15 | window.postMessage({ type: 'SS_UI_REQUEST', text: 'start' }, '*');
16 | });
17 |
18 | // listen for messages from the content-script
19 | window.addEventListener('message', event => {
20 | const { data: { type, streamId }, origin } = event;
21 |
22 | // NOTE: you should discard foreign events
23 | if (origin !== window.location.origin) {
24 | console.warn(
25 | 'ScreenStream: you should discard foreign event from origin:',
26 | origin
27 | );
28 | // return;
29 | }
30 |
31 | // content-script will send a 'SS_PING' msg if extension is installed
32 | if (type === 'SS_PING') {
33 | extensionInstalled = true;
34 | }
35 |
36 | // user chose a stream
37 | if (type === 'SS_DIALOG_SUCCESS') {
38 | startScreenStreamFrom(streamId);
39 | }
40 |
41 | // user clicked on 'cancel' in choose media dialog
42 | if (type === 'SS_DIALOG_CANCEL') {
43 | console.log('User cancelled!');
44 | }
45 | });
46 |
47 | function startScreenStreamFrom(streamId) {
48 | navigator.mediaDevices
49 | .getUserMedia({
50 | audio: false,
51 | video: {
52 | mandatory: {
53 | chromeMediaSource: 'desktop',
54 | chromeMediaSourceId: streamId,
55 | maxWidth: window.screen.width,
56 | maxHeight: window.screen.height
57 | }
58 | }
59 | })
60 | .then(stream => {
61 | videoElement = document.getElementById('video');
62 | videoElement.srcObject = stream;
63 | })
64 | .catch(console.error);
65 | }
66 |
--------------------------------------------------------------------------------
/extension/background.js:
--------------------------------------------------------------------------------
1 | let desktopMediaRequestId = '';
2 |
3 | chrome.runtime.onConnect.addListener(port => {
4 | port.onMessage.addListener(msg => {
5 | if (msg.type === 'SS_UI_REQUEST') {
6 | requestScreenSharing(port, msg);
7 | }
8 | if (msg.type === 'SS_UI_CANCEL') {
9 | cancelScreenSharing(msg);
10 | }
11 | });
12 | });
13 |
14 | function requestScreenSharing(port, msg) {
15 | // https://developer.chrome.com/extensions/desktopCapture
16 | // params:
17 | // - 'data_sources' Set of sources that should be shown to the user.
18 | // - 'targetTab' Tab for which the stream is created.
19 | // - 'streamId' String that can be passed to getUserMedia() API
20 | // Also available:
21 | // ['screen', 'window', 'tab', 'audio']
22 | const sources = ['screen', 'window', 'tab', 'audio'];
23 | const tab = port.sender.tab;
24 |
25 | desktopMediaRequestId = chrome.desktopCapture.chooseDesktopMedia(
26 | sources,
27 | port.sender.tab,
28 | streamId => {
29 | if (streamId) {
30 | msg.type = 'SS_DIALOG_SUCCESS';
31 | msg.streamId = streamId;
32 | } else {
33 | msg.type = 'SS_DIALOG_CANCEL';
34 | }
35 | port.postMessage(msg);
36 | }
37 | );
38 | }
39 |
40 | function cancelScreenSharing(msg) {
41 | if (desktopMediaRequestId) {
42 | chrome.desktopCapture.cancelChooseDesktopMedia(desktopMediaRequestId);
43 | }
44 | }
45 |
46 | function flatten(arr) {
47 | return [].concat.apply([], arr);
48 | }
49 |
50 | // This avoids a reload after an installation
51 | chrome.windows.getAll({ populate: true }, windows => {
52 | const details = { file: 'content-script.js', allFrames: true };
53 |
54 | flatten(windows.map(w => w.tabs)).forEach(tab => {
55 | // Skip chrome:// pages
56 | if (tab.url.match(/(chrome):\/\//gi)) {
57 | return;
58 | }
59 |
60 | // https://developer.chrome.com/extensions/tabs#method-executeScript
61 | // Unfortunately I don't know how to skip non authorized pages, and
62 | // executeScript doesn't have an error callback.
63 | chrome.tabs.executeScript(tab.id, details, () => {
64 | const { runtime: { lastError } } = chrome;
65 |
66 | if (
67 | lastError &&
68 | !lastError.message.match(/cannot access contents of url/i)
69 | ) {
70 | console.error(lastError);
71 | }
72 |
73 | console.log('After injection in tab: ', tab);
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/extension/content-script.js:
--------------------------------------------------------------------------------
1 | // https://chromeextensionsdocs.appspot.com/apps/content_scripts#host-page-communication
2 | // - 'content_script' and execution env are isolated from each other
3 | // - In order to communicate we use the DOM (window.postMessage)
4 | //
5 | // app.js | |content-script.js | |background.js
6 | // window.postMessage|------->|port.postMessage |----->| port.onMessage
7 | // | window | | port |
8 | // getUserMedia |<------ |window.postMessage|<-----| port.postMessage
9 | //
10 | window.contentScriptHasRun = false;
11 |
12 | (function() {
13 | // prevent the content script from running multiple times
14 | if (window.contentScriptHasRun) {
15 | return;
16 | }
17 |
18 | window.contentScriptHasRun = true;
19 |
20 | const port = chrome.runtime.connect(chrome.runtime.id);
21 | port.onMessage.addListener(msg => {
22 | window.postMessage(msg, '*');
23 | });
24 |
25 | window.addEventListener(
26 | 'message',
27 | event => {
28 | // Only accept messages from ourselves
29 | if (event.source !== window) {
30 | return;
31 | }
32 | // Only accept events with a data type
33 | if (!event.data.type) {
34 | return;
35 | }
36 |
37 | if (['SS_UI_REQUEST', 'SS_UI_CANCEL'].includes(event.data.type)) {
38 | port.postMessage(event.data);
39 | }
40 | },
41 | false
42 | );
43 |
44 | window.postMessage({ type: 'SS_PING', text: 'start' }, '*');
45 | })();
46 |
--------------------------------------------------------------------------------
/extension/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wpp/ScreenStream/c31d2923d796f30a7cd3b35cc54105d2aadf3952/extension/icon.png
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Screensharing Extension",
3 | "description": "Screensharing Extension for my app",
4 | "version": "1.0.0",
5 | "manifest_version": 2,
6 | "icons": {
7 | "128": "icon.png"
8 | },
9 | "background": {
10 | "scripts": ["background.js"]
11 | },
12 | "content_scripts": [
13 | {
14 | "matches": [""],
15 | "js": ["content-script.js"]
16 | }
17 | ],
18 | "permissions": [
19 | "desktopCapture",
20 | "file://*/*",
21 | "tabs"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/images/extensions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wpp/ScreenStream/c31d2923d796f30a7cd3b35cc54105d2aadf3952/images/extensions.png
--------------------------------------------------------------------------------
/images/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wpp/ScreenStream/c31d2923d796f30a7cd3b35cc54105d2aadf3952/images/preview.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 | Share Screen
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------