diff --git a/atom/browser/api/atom_api_desktop_capturer.cc b/atom/browser/api/atom_api_desktop_capturer.cc index 96786d09891..f3e05486e9b 100644 --- a/atom/browser/api/atom_api_desktop_capturer.cc +++ b/atom/browser/api/atom_api_desktop_capturer.cc @@ -5,7 +5,6 @@ #include "atom/browser/api/atom_api_desktop_capturer.h" #include "atom/common/api/atom_api_native_image.h" -#include "atom/common/native_mate_converters/callback.h" #include "atom/common/node_includes.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/media/desktop_media_list.h" @@ -25,7 +24,8 @@ struct Converter { content::DesktopMediaID id = source.id; dict.Set("name", base::UTF16ToUTF8(source.name)); dict.Set("id", id.ToString()); - dict.Set("thumbnail", + dict.Set( + "thumbnail", atom::api::NativeImage::Create(isolate, gfx::Image(source.thumbnail))); return ConvertToV8(isolate, dict); } @@ -38,15 +38,29 @@ namespace atom { namespace api { namespace { -// The wrapDesktopCapturer funtion which is implemented in JavaScript -using WrapDesktopCapturerCallback = base::Callback)>; -WrapDesktopCapturerCallback g_wrap_desktop_capturer; - const int kThumbnailWidth = 150; const int kThumbnailHeight = 150; } // namespace -DesktopCapturer::DesktopCapturer(bool show_screens, bool show_windows) { +DesktopCapturer::DesktopCapturer() { +} + +DesktopCapturer::~DesktopCapturer() { +} + +void DesktopCapturer::StartUpdating(const std::vector& sources) { + bool show_screens = false; + bool show_windows = false; + for (const auto& source_type : sources) { + if (source_type == "screen") + show_screens = true; + else if (source_type == "window") + show_windows = true; + } + + if (!show_windows && !show_screens) + return; + webrtc::DesktopCaptureOptions options = webrtc::DesktopCaptureOptions::CreateDefault(); @@ -70,54 +84,39 @@ DesktopCapturer::DesktopCapturer(bool show_screens, bool show_windows) { media_list_->StartUpdating(this); } -DesktopCapturer::~DesktopCapturer() { -} - -const DesktopMediaList::Source& DesktopCapturer::GetSource(int index) { - return media_list_->GetSource(index); +void DesktopCapturer::StopUpdating() { + media_list_.reset(); } void DesktopCapturer::OnSourceAdded(int index) { - Emit("source-added", index); + Emit("source-added", media_list_->GetSource(index)); } void DesktopCapturer::OnSourceRemoved(int index) { - Emit("source-removed", index); + Emit("source-removed", media_list_->GetSource(index)); } void DesktopCapturer::OnSourceMoved(int old_index, int new_index) { - Emit("source-moved", old_index, new_index); } void DesktopCapturer::OnSourceNameChanged(int index) { - Emit("source-name-changed", index); + Emit("source-name-changed", media_list_->GetSource(index)); } void DesktopCapturer::OnSourceThumbnailChanged(int index) { - Emit("source-thumbnail-changed", index); + Emit("source-thumbnail-changed", media_list_->GetSource(index)); } mate::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder( v8::Isolate* isolate) { return mate::ObjectTemplateBuilder(isolate) - .SetMethod("getSource", &DesktopCapturer::GetSource); -} - -void SetWrapDesktopCapturer(const WrapDesktopCapturerCallback& callback) { - g_wrap_desktop_capturer = callback; -} - -void ClearWrapDesktopCapturer() { - g_wrap_desktop_capturer.Reset(); + .SetMethod("startUpdating", &DesktopCapturer::StartUpdating) + .SetMethod("stopUpdating", &DesktopCapturer::StopUpdating); } // static -mate::Handle DesktopCapturer::Create(v8::Isolate* isolate, - bool show_screens, bool show_windows) { - auto handle = mate::CreateHandle(isolate, - new DesktopCapturer(show_screens, show_windows)); - g_wrap_desktop_capturer.Run(handle.ToV8()); - return handle; +mate::Handle DesktopCapturer::Create(v8::Isolate* isolate) { + return mate::CreateHandle(isolate, new DesktopCapturer); } } // namespace api @@ -130,9 +129,7 @@ void Initialize(v8::Local exports, v8::Local unused, v8::Local context, void* priv) { v8::Isolate* isolate = context->GetIsolate(); mate::Dictionary dict(isolate, exports); - dict.SetMethod("_setWrapDesktopCapturer", &atom::api::SetWrapDesktopCapturer); - dict.SetMethod("_clearWrapDesktopCapturer", - &atom::api::ClearWrapDesktopCapturer); + dict.Set("desktopCapturer", atom::api::DesktopCapturer::Create(isolate)); } } // namespace diff --git a/atom/browser/api/atom_api_desktop_capturer.h b/atom/browser/api/atom_api_desktop_capturer.h index aae0a8268b9..f4be410ed61 100644 --- a/atom/browser/api/atom_api_desktop_capturer.h +++ b/atom/browser/api/atom_api_desktop_capturer.h @@ -2,8 +2,11 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. -#ifndef ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_ -#define ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_ +#ifndef ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_H_ +#define ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_H_ + +#include +#include #include "base/memory/scoped_ptr.h" #include "atom/browser/api/event_emitter.h" @@ -18,13 +21,13 @@ namespace api { class DesktopCapturer: public mate::EventEmitter, public DesktopMediaListObserver { public: - static mate::Handle Create(v8::Isolate* isolate, - bool show_screens, bool show_windows); + static mate::Handle Create(v8::Isolate* isolate); - const DesktopMediaList::Source& GetSource(int index); + void StartUpdating(const std::vector& sources); + void StopUpdating(); protected: - DesktopCapturer(bool show_screens, bool show_windows); + DesktopCapturer(); ~DesktopCapturer(); // DesktopMediaListObserver overrides. @@ -48,4 +51,4 @@ class DesktopCapturer: public mate::EventEmitter, } // namespace atom -#endif // ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_ +#endif // ATOM_BROWSER_API_ATOM_API_DESKTOP_CAPTURER_H_ diff --git a/atom/browser/api/atom_api_screen.cc b/atom/browser/api/atom_api_screen.cc index 761ef28ef97..b73bda9ced8 100644 --- a/atom/browser/api/atom_api_screen.cc +++ b/atom/browser/api/atom_api_screen.cc @@ -56,21 +56,6 @@ Screen::~Screen() { screen_->RemoveObserver(this); } -mate::Handle Screen::GetDesktopCapturer( - const std::vector& sources) { - bool show_screens = false; - bool show_windows = false; - for (const auto& source_type : sources) { - if (source_type == "screen") { - show_screens = true; - } else if (source_type == "window") { - show_windows = true; - } - } - - return DesktopCapturer::Create(isolate(), show_screens, show_windows); -} - gfx::Point Screen::GetCursorScreenPoint() { return screen_->GetCursorScreenPoint(); } @@ -122,8 +107,7 @@ mate::ObjectTemplateBuilder Screen::GetObjectTemplateBuilder( .SetMethod("getPrimaryDisplay", &Screen::GetPrimaryDisplay) .SetMethod("getAllDisplays", &Screen::GetAllDisplays) .SetMethod("getDisplayNearestPoint", &Screen::GetDisplayNearestPoint) - .SetMethod("getDisplayMatching", &Screen::GetDisplayMatching) - .SetMethod("getDesktopCapturer", &Screen::GetDesktopCapturer); + .SetMethod("getDisplayMatching", &Screen::GetDisplayMatching); } // static diff --git a/atom/browser/api/atom_api_screen.h b/atom/browser/api/atom_api_screen.h index 619f6c8b3f7..f724130fa7f 100644 --- a/atom/browser/api/atom_api_screen.h +++ b/atom/browser/api/atom_api_screen.h @@ -6,9 +6,7 @@ #define ATOM_BROWSER_API_ATOM_API_SCREEN_H_ #include -#include -#include "atom/browser/api/atom_api_desktop_capturer.h" #include "atom/browser/api/event_emitter.h" #include "native_mate/handle.h" #include "ui/gfx/display_observer.h" @@ -38,9 +36,6 @@ class Screen : public mate::EventEmitter, gfx::Display GetDisplayNearestPoint(const gfx::Point& point); gfx::Display GetDisplayMatching(const gfx::Rect& match_rect); - mate::Handle GetDesktopCapturer( - const std::vector& sources); - // gfx::DisplayObserver: void OnDisplayAdded(const gfx::Display& new_display) override; void OnDisplayRemoved(const gfx::Display& old_display) override; diff --git a/atom/browser/api/lib/app.coffee b/atom/browser/api/lib/app.coffee index 9ff568cf8d2..18c80dc2b1f 100644 --- a/atom/browser/api/lib/app.coffee +++ b/atom/browser/api/lib/app.coffee @@ -3,18 +3,17 @@ EventEmitter = require('events').EventEmitter bindings = process.atomBinding 'app' sessionBindings = process.atomBinding 'session' downloadItemBindings = process.atomBinding 'download_item' -desktopCapturerBindings = process.atomBinding 'desktop_capturer' app = bindings.app app.__proto__ = EventEmitter.prototype -wrapToEventListener = (item) -> - # item is an Event Emitter. - item.__proto__ = EventEmitter.prototype +wrapSession = (session) -> + # session is an Event Emitter. + session.__proto__ = EventEmitter.prototype wrapDownloadItem = (download_item) -> # download_item is an Event Emitter. - wrapToEventListener download_item + download_item.__proto__ = EventEmitter.prototype # Be compatible with old APIs. download_item.url = download_item.getUrl() download_item.filename = download_item.getFilename() @@ -59,14 +58,11 @@ app.resolveProxy = -> @defaultSession.resolveProxy.apply @defaultSession, argume app.on 'activate', (event, hasVisibleWindows) -> @emit 'activate-with-no-open-windows' if not hasVisibleWindows # Session wrapper. -sessionBindings._setWrapSession wrapToEventListener +sessionBindings._setWrapSession wrapSession process.once 'exit', sessionBindings._clearWrapSession downloadItemBindings._setWrapDownloadItem wrapDownloadItem process.once 'exit', downloadItemBindings._clearWrapDownloadItem -desktopCapturerBindings._setWrapDesktopCapturer wrapToEventListener -process.once 'exit', desktopCapturerBindings._clearWrapDesktopCapturer - # Only one App object pemitted. module.exports = app diff --git a/atom/browser/lib/desktop-capturer.coffee b/atom/browser/lib/desktop-capturer.coffee new file mode 100644 index 00000000000..29bb1ae6051 --- /dev/null +++ b/atom/browser/lib/desktop-capturer.coffee @@ -0,0 +1,42 @@ +ipc = require 'ipc' +BrowserWindow = require 'browser-window' +EventEmitter = require('events').EventEmitter + +# The browser module manages all desktop-capturer moduels in renderer process. +desktopCapturer = process.atomBinding('desktop_capturer').desktopCapturer +desktopCapturer.__proto__ = EventEmitter.prototype + +getWebContentsFromId = (id) -> + windows = BrowserWindow.getAllWindows() + return window.webContents for window in windows when window.webContents?.getId() == id + +# The set for tracking id of webContents. +webContentsIds = new Set + +stopDesktopCapture = (id) -> + webContentsIds.delete id + if webContentsIds.size is 0 + desktopCapturer.stopUpdating() + +ipc.on 'ATOM_BROWSER_DESKTOP_CAPTURER_REQUIRED', (event) -> + id = event.sender.getId() + # Remove the tracked webContents when it is destroyed. + getWebContentsFromId(id).on 'destroyed', ()-> + stopDesktopCapture id + event.returnValue = 'done' + +# Handle `desktopCapturer.startUpdating` API. +ipc.on 'ATOM_BROWSER_DESKTOP_CAPTURER_START_UPDATING', (event, args) -> + id = event.sender.getId() + webContentsIds.add id + desktopCapturer.startUpdating args + +# Handle `desktopCapturer.stopUpdating` API. +ipc.on 'ATOM_BROWSER_DESKTOP_CAPTURER_STOP_UPDATING', (event) -> + stopDesktopCapture event.sender.getId() + +for event_name in ['source-added', 'source-removed', 'source-name-changed', "source-thumbnail-changed"] + do (event_name) -> + desktopCapturer.on event_name, (event, source) -> + webContentsIds.forEach (id) -> + getWebContentsFromId(id).send event_name, { id: source.id, name: source.name, dataUrl: source.thumbnail.toDataUrl() } diff --git a/atom/browser/lib/init.coffee b/atom/browser/lib/init.coffee index 1299364d2fa..6caaaf4bf05 100644 --- a/atom/browser/lib/init.coffee +++ b/atom/browser/lib/init.coffee @@ -92,6 +92,9 @@ app.setAppPath packagePath # Load the chrome extension support. require './chrome-extension' +# Load internal desktop-capturer module. +require './desktop-capturer' + # Set main startup script of the app. mainStartupScript = packageJson.main or 'index.js' diff --git a/atom/renderer/api/lib/desktop-capturer.coffee b/atom/renderer/api/lib/desktop-capturer.coffee new file mode 100644 index 00000000000..bf2d27597a0 --- /dev/null +++ b/atom/renderer/api/lib/desktop-capturer.coffee @@ -0,0 +1,22 @@ +ipc = require 'ipc' +remote = require 'remote' +NativeImage = require 'native-image' + +EventEmitter = require('events').EventEmitter +desktopCapturer = new EventEmitter + +# Tells main process the renderer is requiring 'desktop-capture' module. +ipc.sendSync 'ATOM_BROWSER_DESKTOP_CAPTURER_REQUIRED' + +desktopCapturer.startUpdating = (args) -> + ipc.send 'ATOM_BROWSER_DESKTOP_CAPTURER_START_UPDATING', args + +desktopCapturer.stopUpdating = () -> + ipc.send 'ATOM_BROWSER_DESKTOP_CAPTURER_STOP_UPDATING' + +for event_name in ['source-added', 'source-removed', 'source-name-changed', "source-thumbnail-changed"] + do (event_name) -> + ipc.on event_name, (source) -> + desktopCapturer.emit event_name, { id: source.id, name: source.name, thumbnail: NativeImage.createFromDataUrl source.dataUrl } + +module.exports = desktopCapturer diff --git a/chromium_src/chrome/browser/media/native_desktop_media_list.cc b/chromium_src/chrome/browser/media/native_desktop_media_list.cc index 1db22ab72db..050c5e1d5a7 100644 --- a/chromium_src/chrome/browser/media/native_desktop_media_list.cc +++ b/chromium_src/chrome/browser/media/native_desktop_media_list.cc @@ -297,8 +297,8 @@ void NativeDesktopMediaList::OnSourcesList( // Iterate through the old sources to find the removed sources. for (size_t i = 0; i < sources_.size(); ++i) { if (new_source_set.find(sources_[i].id) == new_source_set.end()) { - sources_.erase(sources_.begin() + i); observer_->OnSourceRemoved(i); + sources_.erase(sources_.begin() + i); --i; } } diff --git a/docs/README.md b/docs/README.md index b2f95fe4b57..cc25e9d0ff6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,6 +46,7 @@ ### Modules for the Renderer Process (Web Page): +* [desktop-capturer](api/desktop-capturer.md) * [ipc (renderer)](api/ipc-renderer.md) * [remote](api/remote.md) * [web-frame](api/web-frame.md) diff --git a/docs/api/desktop-capturer.md b/docs/api/desktop-capturer.md new file mode 100644 index 00000000000..8862762ddab --- /dev/null +++ b/docs/api/desktop-capturer.md @@ -0,0 +1,118 @@ +# desktop-capturer + +The `desktop-capturer` is a renderer module used to capture the content of +screen and individual app windows. + +```javascript +// In the renderer process. +var desktopCapturer = require('desktop-capturer'); + +desktopCapturer.on('source-added', function(source) { + console.log("source " + source.name + " is added."); + // source.thumbnail is not ready to use here and webkitGetUserMedia neither. +}); + +desktopCapturer.on('source-thumbnail-changed', function(source) { + if (source.name == "Electron") { + // stopUpdating since we have found the window that we want to capture. + desktopCapturer.stopUpdating(); + + // It's ready to use webkitGetUserMedia right now. + navigator.webkitGetUserMedia({ + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: source.id, + minWidth: 1280, + maxWidth: 1280, + minHeight: 720, + maxHeight: 720 + } + } + }, gotStream, getUserMediaError); + } +}); + +// Let's start updating after setting all event of `desktopCapturer` +desktopCapturer.startUpdating(); + +function gotStream(stream) { + document.querySelector('video').src = URL.createObjectURL(stream); +} + +function getUserMediaError(e) { + console.log('getUserMediaError'); +} +``` + +## Events + +### Event: 'source-added' + +* `source` Source + +Emits when there is a new source added, usually a new window is created, +a new screen is attached. + +**Note:** The thumbnail of the source is not ready for use when 'source-added' +event is emitted, and `navigator.webkitGetUserMedia` neither. + +### Event: 'source-removed' + +* `source` Source + +Emits when there is a source removed. + +### Event: 'source-name-changed' + +* `source` Source + +Emits when the name of source is changed. + +### Event: 'source-thumbnail-changed' + +* `source` Source + +Emits when the thumbnail of source is changed. `desktopCapturer` will refresh +all sources every second. + +## Methods + +The `desktopCapturer` module has the following methods: + +### `desktopCapturer.startUpdating(options)` + +* `options` Array - An array of String that enums the types of desktop sources. + * `screen` String - Screen + * `window` String - Individual window + +Starts updating desktopCapturer. The events of `desktopCapturer` will only be +emitted after `startUpdating` API is invoked. + +**Note:** At beginning, the desktopCapturer is initially empty, so the +`source-added` event will be emitted for each existing source as it is +enumrated. +On Windows, you will see the screen ficker when desktopCapturer starts updating. +This is normal because the desktop effects(e.g. Aero) will be disabled when +desktop capturer is working. The ficker will disappear once +`desktopCapturer.stopUpdating()` is invoked. + +### `desktopCapturer.stopUpdating()` + +Stops updating desktopCapturer. The events of `desktopCapturer` will not be +emitted after the API is invoked. + +**Note:** It is a good practice to call `stopUpdating` when you do not need +getting any infomation of sources, usually after passing a id of source to +`navigator.webkitGetUserMedia`. + +## Source + +`Source` is an object represents a captured screen or individual window. It has +following properties: + +* `id` String - The id of the capturing window or screen used in + `navigator.webkitGetUserMedia`. +* `name` String - The descriped name of the capturing screen or window. +* `thumbnail` [NativeImage](NativeImage.md) - A thumbnail image. diff --git a/filenames.gypi b/filenames.gypi index 81d4d87d3aa..46e06bebd18 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -26,6 +26,7 @@ 'atom/browser/api/lib/tray.coffee', 'atom/browser/api/lib/web-contents.coffee', 'atom/browser/lib/chrome-extension.coffee', + 'atom/browser/lib/desktop-capturer.coffee', 'atom/browser/lib/guest-view-manager.coffee', 'atom/browser/lib/guest-window-manager.coffee', 'atom/browser/lib/init.coffee', @@ -45,6 +46,7 @@ 'atom/renderer/lib/web-view/web-view.coffee', 'atom/renderer/lib/web-view/web-view-attributes.coffee', 'atom/renderer/lib/web-view/web-view-constants.coffee', + 'atom/renderer/api/lib/desktop-capturer.coffee', 'atom/renderer/api/lib/ipc.coffee', 'atom/renderer/api/lib/remote.coffee', 'atom/renderer/api/lib/screen.coffee',