├── libs ├── NDIlib.exp ├── NDIlib.lib ├── NDIlib.cp37-win_amd64.pyd ├── Processing.NDI.Lib.x64.lib ├── Processing.NDI.Lib.UWP.x64.dll ├── Processing.NDI.Lib.UWP.x64.lib ├── readme.md ├── pyinstaller.spec ├── pyinstaller.py ├── wxpython.py ├── Processing.NDI.Lib.Licenses.txt └── hook-cefpython3.py ├── images ├── chromicast.icns ├── chromicast.ico └── readme.md ├── Chromicast.spec ├── readme.md ├── LICENSE ├── src └── chromicast.py └── chromicast.py /libs/NDIlib.exp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/NDIlib.exp -------------------------------------------------------------------------------- /libs/NDIlib.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/NDIlib.lib -------------------------------------------------------------------------------- /images/chromicast.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/images/chromicast.icns -------------------------------------------------------------------------------- /images/chromicast.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/images/chromicast.ico -------------------------------------------------------------------------------- /libs/NDIlib.cp37-win_amd64.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/NDIlib.cp37-win_amd64.pyd -------------------------------------------------------------------------------- /libs/Processing.NDI.Lib.x64.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/Processing.NDI.Lib.x64.lib -------------------------------------------------------------------------------- /libs/Processing.NDI.Lib.UWP.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/Processing.NDI.Lib.UWP.x64.dll -------------------------------------------------------------------------------- /libs/Processing.NDI.Lib.UWP.x64.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/chromicast/HEAD/libs/Processing.NDI.Lib.UWP.x64.lib -------------------------------------------------------------------------------- /images/readme.md: -------------------------------------------------------------------------------- 1 | Icon files. 2 | 3 | I was making different versions of the code to explore different opportunties for its use. 4 | -------------------------------------------------------------------------------- /libs/readme.md: -------------------------------------------------------------------------------- 1 | Download the SDK NDI files from here i think. 2 | 3 | https://www.ndi.tv/sdk/ 4 | 5 | Any NDI files included here that are owned by NewTek have their own license and are not covered by any license offered by Steve Seguin. 6 | 7 | This folder contains NDI files, along with some files that I was using to build executable versions of the Python script with. Please note the licenses may vary between files, as not all files are going to be MIT licensed,etc. 8 | -------------------------------------------------------------------------------- /Chromicast.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | # MIT License 3 | # Copyright (c) 2023 Steve Seguin 4 | 5 | block_cipher = None 6 | 7 | 8 | a = Analysis(['src/chromicast.py'], 9 | pathex=['/Users/XXXXXX/Desktop/chromicast'], 10 | binaries=[], 11 | datas=[('/usr/local/lib/python3.7/site-packages/cefpython3', 'cefpython3')], 12 | hiddenimports=['pkg_resources.py2_warn'], 13 | hookspath=[], 14 | runtime_hooks=[], 15 | excludes=[], 16 | win_no_prefer_redirects=False, 17 | win_private_assemblies=False, 18 | cipher=block_cipher, 19 | noarchive=False) 20 | pyz = PYZ(a.pure, a.zipped_data, 21 | cipher=block_cipher) 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | [], 28 | name='Chromicast', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=True, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=True , icon='images/chromicast.ico') 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Website (webRTC) to NDI source headlessly 2 | 3 | It works enough to output a website (include VDO.Ninja streams) into an NDI Video output. It can do this headlessly. 4 | 5 | This code is me tinkering around with ideas; it is unfinished. Audio isn't yet supported. 6 | 7 | It is based on the Chromicam repo I made earlier (https://github.com/steveseguin/chromicam ), but outputs to NDI instead of Virtualcam. 8 | 9 | This repo requires a couple dependencies 10 | 11 | ``` 12 | cefpython3>=66.0 13 | numpy>=1.14.0 14 | ``` 15 | 16 | and you'll need the NDI SDK and the following, which requires compiling. 17 | ``` 18 | https://github.com/buresu/ndi-python 19 | ``` 20 | 21 | After that, you can run with just python as a script, or you can compile into an app. 22 | 23 | example way to run using a VDO.Ninja stream as a source: 24 | 25 | `python3 chromicast.py https://vdo.ninja/?view=xxxxx&codec=vp8 1280 720` 26 | 27 | ## compiling (optional) 28 | 29 | If you want to try building Chromicast, the following is the basic idea, but good luck! Dependencies are a pain to sort out well. 30 | 31 | ### macOS Build 32 | ``` 33 | python3 build chromicast.py pack 34 | ``` 35 | ## Windows Build 36 | ``` 37 | pyinstaller --onefile --hidden-import='pkg_resources.py2_warn' --icon=chromicast.ico chromicast.py 38 | ``` 39 | ### find the location of CEF on macOS 40 | ``` 41 | sudo find / | grep "Chromium Embedded Framework" 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The chromicast.py file is licensed under the permissive MIT License. 2 | 3 | Other files in this repository may have a different license and/or different license holder. 4 | 5 | I've added this license three years after last working on the code base. 6 | You'd be best to double check all code and licenses before using, in case I made any mistakes. 7 | 8 | #### 9 | 10 | MIT License 11 | 12 | Copyright (c) 2023 Steve Seguin 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /libs/pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | This is a PyInstaller spec file. 6 | This file was modified by Steve, but it was not originally by steve 7 | """ 8 | 9 | import os 10 | from PyInstaller.building.api import PYZ, EXE, COLLECT 11 | from PyInstaller.building.build_main import Analysis 12 | from PyInstaller.utils.hooks import is_module_satisfies 13 | from PyInstaller.archive.pyz_crypto import PyiBlockCipher 14 | 15 | # Constants 16 | DEBUG = os.environ.get("CEFPYTHON_PYINSTALLER_DEBUG", False) 17 | PYCRYPTO_MIN_VERSION = "2.6.1" 18 | 19 | # Set this secret cipher to some secret value. It will be used 20 | # to encrypt archive package containing your app's bytecode 21 | # compiled Python modules, to make it harder to extract these 22 | # files and decompile them. If using secret cipher then you 23 | # must install pycrypto package by typing: "pip install pycrypto". 24 | # Note that this will only encrypt archive package containing 25 | # imported modules, it won't encrypt the main script file 26 | # (wxpython.py). The names of all imported Python modules can be 27 | # still accessed, only their contents are encrypted. 28 | SECRET_CIPHER = "This-is-a-secret-phrase" # Only first 16 chars are used 29 | 30 | # ---------------------------------------------------------------------------- 31 | # Main 32 | # ---------------------------------------------------------------------------- 33 | 34 | cipher_obj = None 35 | 36 | a = Analysis( 37 | ["./wxpython.py"], 38 | hookspath=["."], # To find "hook-cefpython3.py" 39 | cipher=cipher_obj, 40 | win_private_assemblies=True, 41 | win_no_prefer_redirects=True, 42 | ) 43 | 44 | if not os.environ.get("PYINSTALLER_CEFPYTHON3_HOOK_SUCCEEDED", None): 45 | raise SystemExit("Error: Pyinstaller hook-cefpython3.py script was " 46 | "not executed or it failed") 47 | 48 | pyz = PYZ(a.pure, 49 | a.zipped_data, 50 | cipher=cipher_obj) 51 | 52 | exe = EXE(pyz, 53 | a.scripts, 54 | exclude_binaries=True, 55 | name="chromicast", 56 | debug=DEBUG, 57 | strip=False, 58 | upx=False, 59 | console=DEBUG, 60 | onefile=True, 61 | windowed=True, 62 | icon="./images/chromicast.ico") 63 | 64 | COLLECT(exe, 65 | a.binaries, 66 | a.zipfiles, 67 | a.datas, 68 | strip=False, 69 | upx=False, 70 | name="chromicast") 71 | -------------------------------------------------------------------------------- /libs/pyinstaller.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example of using PyInstaller packager to build 3 | executable from one of CEF Python's examples (wxpython.py). 4 | 5 | See README-pyinstaller.md for installing required packages. 6 | 7 | To package example type: `python pyinstaller.py`. 8 | """ 9 | # this file was modified by steve, but its not originally be steve 10 | 11 | import os 12 | import platform 13 | import shutil 14 | import sys 15 | from subprocess import Popen 16 | 17 | try: 18 | import PyInstaller 19 | except ImportError: 20 | PyInstaller = None 21 | raise SystemExit("Error: PyInstaller package missing. " 22 | "To install type: pip install --upgrade pyinstaller") 23 | 24 | EXE_EXT = "" 25 | if platform.system() == "Windows": 26 | EXE_EXT = ".exe" 27 | elif platform.system() == "Darwin": 28 | EXE_EXT = ".app" 29 | elif platform.system() == "Linux": 30 | EXE_EXT = "" 31 | 32 | 33 | def main(): 34 | # Platforms supported 35 | if platform.system() not in ["Windows", "Darwin", "Linux"]: 36 | raise SystemExit("Error: Only Windows, Linux and Darwin platforms are " 37 | "currently supported. See Issue #135 for details.") 38 | 39 | # Make sure nothing is cached from previous build. 40 | # Delete the build/ and dist/ directories. 41 | if os.path.exists("build/"): 42 | shutil.rmtree("build/") 43 | if os.path.exists("dist/"): 44 | shutil.rmtree("dist/") 45 | 46 | # Execute pyinstaller. 47 | # Note: the "--clean" flag passed to PyInstaller will delete 48 | # global global cache and temporary files from previous 49 | # runs. For example on Windows this will delete the 50 | # "%appdata%/roaming/pyinstaller/bincache00_py27_32bit" 51 | # directory. 52 | env = os.environ 53 | if "--debug" in sys.argv: 54 | env["CEFPYTHON_PYINSTALLER_DEBUG"] = "1" 55 | sub = Popen(["pyinstaller", "--clean", "--onefile", "--windowed", "--icon", "images/chromicast.ico", "pyinstaller.spec"], env=env) 56 | sub.communicate() 57 | rcode = sub.returncode 58 | if rcode != 0: 59 | print("Error: PyInstaller failed, code=%s" % rcode) 60 | # Delete distribution directory if created 61 | if os.path.exists("dist/"): 62 | shutil.rmtree("dist/") 63 | sys.exit(1) 64 | 65 | # Make sure everything went fine 66 | curdir = os.path.dirname(os.path.abspath(__file__)) 67 | cefapp_dir = os.path.join(curdir, "dist", "chromicast") 68 | executable = os.path.join(cefapp_dir, "chromicast"+EXE_EXT) 69 | if not os.path.exists(executable): 70 | print("Error: PyInstaller failed, main executable is missing: %s" 71 | % executable) 72 | sys.exit(1) 73 | 74 | # Done 75 | print("OK. Created dist/ directory.") 76 | 77 | # On Windows open folder in explorer or when --debug is passed 78 | # run the result binary using "cmd.exe /k chromicast.exe", so that 79 | # console window doesn't close. 80 | if platform.system() == "Windows": 81 | if "--debug" in sys.argv: 82 | os.system("start cmd /k \"%s\"" % executable) 83 | else: 84 | # SYSTEMROOT = C:/Windows 85 | os.system("%s/explorer.exe /n,/e,%s" % ( 86 | os.environ["SYSTEMROOT"], cefapp_dir)) 87 | 88 | 89 | if __name__ == "__main__": 90 | main() 91 | -------------------------------------------------------------------------------- /src/chromicast.py: -------------------------------------------------------------------------------- 1 | ''' 2 | MIT License 3 | 4 | Copyright (c) 2023 Steve Seguin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ''' 24 | 25 | # python3 chromicam.py https://obs.ninja/?view=xxxxx 1280 720 26 | import os 27 | import sys 28 | import numpy as np 29 | import time 30 | import threading 31 | import NDIlib as ndi 32 | 33 | from cefpython3 import cefpython as cef 34 | 35 | # Config 36 | URL = "https://obs.ninja/chromicam" 37 | VIEWPORT_SIZE = (1280, 720) 38 | FRAMERATE = 30 39 | 40 | 41 | def command_line_arguments(): 42 | if len(sys.argv) > 1: 43 | url = sys.argv[1] 44 | if url.startswith("http://") or url.startswith("https://"): 45 | global URL 46 | URL = url 47 | else: 48 | print("Error: Invalid url argument") 49 | sys.exit(1) 50 | if len(sys.argv) > 3: 51 | width = int(sys.argv[2]) 52 | height = int(sys.argv[3]) 53 | if width > 0 and height > 0: 54 | global VIEWPORT_SIZE 55 | VIEWPORT_SIZE = (width, height) 56 | else: 57 | print("Error: Invalid width and height") 58 | sys.exit(1) 59 | if len(sys.argv) > 4: 60 | global FRAMERATE 61 | FRAMERATE = int(sys.argv[4]) 62 | 63 | 64 | 65 | class LoadHandler(object): 66 | def OnLoadingStateChange(self, browser, is_loading, **_): 67 | pass 68 | 69 | def drawFrame(browser, run_event, camera_ready): 70 | time.sleep(2) 71 | 72 | if not ndi.initialize(): 73 | return 0 74 | 75 | send_settings = ndi.SendCreate() 76 | send_settings.ndi_name = URL 77 | 78 | ndi_send = ndi.send_create(send_settings) 79 | 80 | video_frame = ndi.VideoFrameV2() 81 | 82 | #video_frame.data = img 83 | video_frame.FourCC = ndi.FOURCC_VIDEO_TYPE_RGBA 84 | 85 | while run_event.is_set(): 86 | camera_ready.wait() 87 | buffer_string = browser.GetUserData("OnPaint.buffer_string") 88 | if (buffer_string): 89 | 90 | img = np.frombuffer(buffer_string, dtype=np.uint8, count=(VIEWPORT_SIZE[0]*VIEWPORT_SIZE[1]*4)).reshape((VIEWPORT_SIZE[1],VIEWPORT_SIZE[0],4)) 91 | video_frame.data = img 92 | ndi.send_send_video_v2(ndi_send, video_frame) 93 | 94 | camera_ready.clear() 95 | #cam.sleep_until_next_frame() 96 | 97 | class RenderHandler(object): 98 | def __init__(self): 99 | print('Starting to Render Frames') 100 | pass 101 | 102 | def GetViewRect(self, rect_out, **_): 103 | rect_out.extend([0, 0, VIEWPORT_SIZE[0], VIEWPORT_SIZE[1]]) 104 | return True 105 | 106 | def OnPaint(self, browser, element_type, paint_buffer, **_): 107 | global camera_ready 108 | if element_type == cef.PET_VIEW: 109 | buffer_string = paint_buffer.GetString(mode="rgba", origin="top-left") 110 | browser.SetUserData("OnPaint.buffer_string", buffer_string) 111 | camera_ready.set(); 112 | 113 | global t,run_event, camera_ready 114 | 115 | def endProgram(exctype, value, tb): 116 | global t, run_event, camera_ready 117 | print("EXITING"); 118 | run_event.clear() 119 | camera_ready.set() 120 | t.join() 121 | cef.QuitMessageLoop() 122 | browser.CloseBrowser() 123 | cef.Shutdown() 124 | 125 | ndi.send_destroy(ndi_send) 126 | ndi.destroy() 127 | 128 | 129 | try: 130 | sys.exit(0) 131 | except SystemExit: 132 | os._exit(0) 133 | 134 | try: 135 | sys.excepthook = endProgram # To shutdown all CEF processes on error 136 | 137 | command_line_arguments() 138 | settings = { 139 | "windowless_rendering_enabled": True, 140 | 141 | } 142 | if hasattr(sys, 'frozen') and 'darwin' in sys.platform: 143 | settings["framework_dir_path"] = os.path.abspath("/usr/local/lib/python3.7/site-packages/cefpython3/Chromium Embedded Framework.framework") 144 | settings["browser_subprocess_path"] = os.path.abspath("/usr/local/lib/python3.7/site-packages/cefpython3/subprocess") 145 | settings["debug"] = False 146 | 147 | switches = { 148 | "disable-gpu": "", 149 | "disable-gpu-compositing": "", 150 | "enable-media-stream": "", 151 | "autoplay-policy":"no-user-gesture-required", 152 | "enable-begin-frame-scheduling": "", 153 | "disable-surfaces": "", # This is required for PDF ext to work 154 | } 155 | browser_settings = { 156 | "windowless_frame_rate": 30, # Default frame rate in CEF is 30 157 | } 158 | cef.Initialize(settings=settings, switches=switches) 159 | 160 | parent_window_handle = 0 161 | window_info = cef.WindowInfo() 162 | window_info.SetAsOffscreen(parent_window_handle) 163 | 164 | camera_ready = threading.Event() 165 | camera_ready.set() 166 | 167 | browser = cef.CreateBrowserSync(window_info=window_info, settings=browser_settings, url=URL) 168 | browser.SetClientHandler(RenderHandler()) 169 | 170 | browser.SendFocusEvent(True) 171 | browser.WasResized() 172 | browser.SetClientHandler(LoadHandler()) 173 | 174 | run_event = threading.Event() 175 | run_event.set() 176 | 177 | t = threading.Thread(target=drawFrame, args=(browser,run_event,camera_ready,)) 178 | t.start() 179 | 180 | cef.MessageLoop() 181 | run_event.clear() 182 | camera_ready.set() 183 | t.join() 184 | 185 | cef.QuitMessageLoop() 186 | browser.CloseBrowser() 187 | cef.Shutdown() 188 | 189 | except: 190 | print('Interrupted') 191 | run_event.clear() 192 | t.join() 193 | 194 | cef.QuitMessageLoop() 195 | browser.CloseBrowser() 196 | cef.Shutdown() 197 | try: 198 | sys.exit(0) 199 | except SystemExit: 200 | os._exit(0) 201 | -------------------------------------------------------------------------------- /chromicast.py: -------------------------------------------------------------------------------- 1 | ''' 2 | MIT License 3 | 4 | Copyright (c) 2023 Steve Seguin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ''' 24 | 25 | # python3 chromicam.py https://vdo.ninja/?view=xxxxx 1280 720 26 | import os 27 | import sys 28 | import numpy as np 29 | import time 30 | import threading 31 | import NDIlib as ndi 32 | 33 | from cefpython3 import cefpython as cef 34 | 35 | # Config 36 | URL = "https://vdo.ninja/chromicam" 37 | VIEWPORT_SIZE = (1280, 720) 38 | FRAMERATE = 30 39 | 40 | 41 | def command_line_arguments(): 42 | if len(sys.argv) > 1: 43 | url = sys.argv[1] 44 | if url.startswith("http://") or url.startswith("https://"): 45 | global URL 46 | URL = url 47 | else: 48 | print("Error: Invalid url argument") 49 | sys.exit(1) 50 | if len(sys.argv) > 3: 51 | width = int(sys.argv[2]) 52 | height = int(sys.argv[3]) 53 | if width > 0 and height > 0: 54 | global VIEWPORT_SIZE 55 | VIEWPORT_SIZE = (width, height) 56 | else: 57 | print("Error: Invalid width and height") 58 | sys.exit(1) 59 | if len(sys.argv) > 4: 60 | global FRAMERATE 61 | FRAMERATE = int(sys.argv[4]) 62 | 63 | 64 | 65 | class LoadHandler(object): 66 | def OnLoadingStateChange(self, browser, is_loading, **_): 67 | pass 68 | 69 | def drawFrame(browser, run_event, camera_ready): 70 | time.sleep(2) 71 | 72 | if not ndi.initialize(): 73 | return 0 74 | 75 | send_settings = ndi.SendCreate() 76 | send_settings.ndi_name = URL 77 | 78 | ndi_send = ndi.send_create(send_settings) 79 | 80 | video_frame = ndi.VideoFrameV2() 81 | 82 | #video_frame.data = img 83 | video_frame.FourCC = ndi.FOURCC_VIDEO_TYPE_RGBA 84 | 85 | 86 | print("Starting Draw Routine..") 87 | while run_event.is_set(): 88 | camera_ready.wait() 89 | buffer_string = browser.GetUserData("OnPaint.buffer_string") 90 | if (buffer_string): 91 | 92 | img = np.frombuffer(buffer_string, dtype=np.uint8, count=(VIEWPORT_SIZE[0]*VIEWPORT_SIZE[1]*4)).reshape((VIEWPORT_SIZE[1],VIEWPORT_SIZE[0],4)) 93 | video_frame.data = img 94 | ndi.send_send_video_v2(ndi_send, video_frame) 95 | 96 | camera_ready.clear() 97 | #cam.sleep_until_next_frame() 98 | 99 | class RenderHandler(object): 100 | def __init__(self): 101 | print('Starting to Render Frames') 102 | pass 103 | 104 | def GetViewRect(self, rect_out, **_): 105 | rect_out.extend([0, 0, VIEWPORT_SIZE[0], VIEWPORT_SIZE[1]]) 106 | return True 107 | 108 | def OnPaint(self, browser, element_type, paint_buffer, **_): 109 | global camera_ready 110 | if element_type == cef.PET_VIEW: 111 | buffer_string = paint_buffer.GetString(mode="rgba", origin="top-left") 112 | browser.SetUserData("OnPaint.buffer_string", buffer_string) 113 | camera_ready.set(); 114 | 115 | global t,run_event, camera_ready 116 | 117 | def endProgram(exctype, value, tb): 118 | global t, run_event, camera_ready 119 | print("EXITING"); 120 | run_event.clear() 121 | camera_ready.set() 122 | t.join() 123 | cef.QuitMessageLoop() 124 | browser.CloseBrowser() 125 | cef.Shutdown() 126 | 127 | ndi.send_destroy(ndi_send) 128 | ndi.destroy() 129 | 130 | 131 | try: 132 | sys.exit(0) 133 | except SystemExit: 134 | os._exit(0) 135 | 136 | try: 137 | sys.excepthook = endProgram # To shutdown all CEF processes on error 138 | 139 | command_line_arguments() 140 | settings = { 141 | "windowless_rendering_enabled": True, 142 | 143 | } 144 | if hasattr(sys, 'frozen') and 'darwin' in sys.platform: 145 | settings["framework_dir_path"] = os.path.abspath("lib/python3.7/cefpython3/Chromium Embedded Framework.framework") 146 | settings["browser_subprocess_path"] = os.path.abspath("lib/python3.7/cefpython3/subprocess") 147 | settings["debug"] = False 148 | 149 | switches = { 150 | "disable-gpu": "", 151 | "disable-gpu-compositing": "", 152 | "enable-media-stream": "", 153 | "autoplay-policy":"no-user-gesture-required", 154 | "enable-begin-frame-scheduling": "", 155 | "disable-surfaces": "", # This is required for PDF ext to work 156 | } 157 | browser_settings = { 158 | "windowless_frame_rate": 30, # Default frame rate in CEF is 30 159 | } 160 | cef.Initialize(settings=settings, switches=switches) 161 | 162 | parent_window_handle = 0 163 | window_info = cef.WindowInfo() 164 | window_info.SetAsOffscreen(parent_window_handle) 165 | 166 | camera_ready = threading.Event() 167 | camera_ready.set() 168 | 169 | browser = cef.CreateBrowserSync(window_info=window_info, settings=browser_settings, url=URL) 170 | browser.SetClientHandler(RenderHandler()) 171 | 172 | browser.SendFocusEvent(True) 173 | browser.WasResized() 174 | browser.SetClientHandler(LoadHandler()) 175 | 176 | run_event = threading.Event() 177 | run_event.set() 178 | 179 | t = threading.Thread(target=drawFrame, args=(browser,run_event,camera_ready,)) 180 | t.start() 181 | 182 | cef.MessageLoop() 183 | run_event.clear() 184 | camera_ready.set() 185 | t.join() 186 | 187 | cef.QuitMessageLoop() 188 | browser.CloseBrowser() 189 | cef.Shutdown() 190 | 191 | except: 192 | print('Interrupted') 193 | run_event.clear() 194 | t.join() 195 | 196 | cef.QuitMessageLoop() 197 | browser.CloseBrowser() 198 | cef.Shutdown() 199 | try: 200 | sys.exit(0) 201 | except SystemExit: 202 | os._exit(0) 203 | -------------------------------------------------------------------------------- /libs/wxpython.py: -------------------------------------------------------------------------------- 1 | ''' 2 | MIT License 3 | 4 | Copyright (c) 2023 Steve Seguin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ''' 24 | 25 | # python3 chromicam.py https://vdo.ninja/?view=xxxxx 1280 720 26 | 27 | import os 28 | import sys 29 | import numpy as np 30 | import time 31 | import threading 32 | import NDIlib as ndi 33 | 34 | from cefpython3 import cefpython as cef 35 | 36 | 37 | import tkinter as tk 38 | 39 | global URL 40 | root= tk.Tk() 41 | URL = "https://google.com" 42 | 43 | VIEWPORT_SIZE = (1280, 720) 44 | FRAMERATE = 30 45 | 46 | ## https://stackoverflow.com/a/43008165 47 | def retrieve_input(): 48 | global URL 49 | URL="https://vdo.ninja/?v="+textBox.get("1.0","end-1c") 50 | root.destroy() 51 | textBox=tk.Text(root, height=1, width=50) 52 | textBox.insert(tk.INSERT, "Paste the stream-ID here then press Start", "a") 53 | textBox.pack() 54 | buttonCommit=tk.Button(root, height=1, width=20, text="Start", 55 | command=lambda: retrieve_input()) 56 | buttonCommit.pack() 57 | 58 | tk.mainloop() 59 | 60 | def hello (): 61 | 62 | global t, run_event, camera_ready 63 | print("EXITING"); 64 | run_event.clear() 65 | cef.QuitMessageLoop() 66 | camera_ready.set() 67 | t.join() 68 | 69 | browser.CloseBrowser() 70 | cef.Shutdown() 71 | ndi.send_destroy(ndi_send) 72 | ndi.destroy() 73 | 74 | def exit_button(): 75 | root= tk.Tk() 76 | canvas1 = tk.Canvas(root, width = 200, height = 100, bg='grey') 77 | canvas1.pack() 78 | button1 = tk.Button(text='Quit',command=hello, bg='blue',fg='white') 79 | canvas1.create_window(100, 50, window=button1) 80 | root.mainloop() 81 | 82 | 83 | t2 = threading.Thread(target=exit_button) 84 | t2.start() 85 | 86 | def command_line_arguments(): 87 | if len(sys.argv) > 1: 88 | url = sys.argv[1] 89 | if url.startswith("http://") or url.startswith("https://"): 90 | global URL 91 | URL = url 92 | else: 93 | print("Error: Invalid url argument") 94 | sys.exit(1) 95 | if len(sys.argv) > 3: 96 | width = int(sys.argv[2]) 97 | height = int(sys.argv[3]) 98 | if width > 0 and height > 0: 99 | global VIEWPORT_SIZE 100 | VIEWPORT_SIZE = (width, height) 101 | else: 102 | print("Error: Invalid width and height") 103 | sys.exit(1) 104 | if len(sys.argv) > 4: 105 | global FRAMERATE 106 | FRAMERATE = int(sys.argv[4]) 107 | 108 | 109 | 110 | class LoadHandler(object): 111 | def OnLoadingStateChange(self, browser, is_loading, **_): 112 | pass 113 | 114 | def drawFrame(browser, run_event, camera_ready): 115 | time.sleep(2) 116 | 117 | if not ndi.initialize(): 118 | return 0 119 | 120 | send_settings = ndi.SendCreate() 121 | send_settings.ndi_name = "ChromiCast" 122 | 123 | ndi_send = ndi.send_create(send_settings) 124 | 125 | video_frame = ndi.VideoFrameV2() 126 | 127 | #video_frame.data = img 128 | video_frame.FourCC = ndi.FOURCC_VIDEO_TYPE_RGBA 129 | 130 | 131 | print("Starting Draw Routine..") 132 | while run_event.is_set(): 133 | 134 | camera_ready.wait() 135 | buffer_string = browser.GetUserData("OnPaint.buffer_string") 136 | if (buffer_string): 137 | 138 | img = np.frombuffer(buffer_string, dtype=np.uint8, count=(VIEWPORT_SIZE[0]*VIEWPORT_SIZE[1]*4)).reshape((VIEWPORT_SIZE[1],VIEWPORT_SIZE[0],4)) 139 | video_frame.data = img 140 | ndi.send_send_video_v2(ndi_send, video_frame) 141 | 142 | camera_ready.clear() 143 | #cam.sleep_until_next_frame() 144 | 145 | class RenderHandler(object): 146 | def __init__(self): 147 | print('Starting to Render Frames') 148 | pass 149 | 150 | def GetViewRect(self, rect_out, **_): 151 | rect_out.extend([0, 0, VIEWPORT_SIZE[0], VIEWPORT_SIZE[1]]) 152 | return True 153 | 154 | def OnPaint(self, browser, element_type, paint_buffer, **_): 155 | global camera_ready 156 | if element_type == cef.PET_VIEW: 157 | buffer_string = paint_buffer.GetString(mode="rgba", origin="top-left") 158 | browser.SetUserData("OnPaint.buffer_string", buffer_string) 159 | camera_ready.set(); 160 | 161 | global t,run_event, camera_ready 162 | 163 | def endProgram(exctype, value, tb): 164 | global t, run_event, camera_ready 165 | print("EXITING"); 166 | run_event.clear() 167 | camera_ready.set() 168 | t.join() 169 | cef.QuitMessageLoop() 170 | browser.CloseBrowser() 171 | cef.Shutdown() 172 | 173 | ndi.send_destroy(ndi_send) 174 | ndi.destroy() 175 | 176 | 177 | try: 178 | sys.exit(0) 179 | except SystemExit: 180 | os._exit(0) 181 | 182 | try: 183 | sys.excepthook = endProgram # To shutdown all CEF processes on error 184 | 185 | command_line_arguments() 186 | settings = { 187 | "windowless_rendering_enabled": True, 188 | } 189 | 190 | switches = { 191 | "disable-gpu": "", 192 | "disable-gpu-compositing": "", 193 | "enable-media-stream": "", 194 | "autoplay-policy":"no-user-gesture-required", 195 | "enable-begin-frame-scheduling": "", 196 | "disable-surfaces": "", # This is required for PDF ext to work 197 | } 198 | browser_settings = { 199 | "windowless_frame_rate": 30, # Default frame rate in CEF is 30 200 | } 201 | cef.Initialize(settings=settings, switches=switches) 202 | 203 | parent_window_handle = 0 204 | window_info = cef.WindowInfo() 205 | window_info.SetAsOffscreen(parent_window_handle) 206 | 207 | camera_ready = threading.Event() 208 | camera_ready.set() 209 | 210 | browser = cef.CreateBrowserSync(window_info=window_info, settings=browser_settings, url=URL) 211 | browser.SetClientHandler(RenderHandler()) 212 | 213 | browser.SendFocusEvent(True) 214 | browser.WasResized() 215 | browser.SetClientHandler(LoadHandler()) 216 | 217 | run_event = threading.Event() 218 | run_event.set() 219 | 220 | t = threading.Thread(target=drawFrame, args=(browser,run_event,camera_ready,)) 221 | t.start() 222 | cef.MessageLoop() 223 | run_event.clear() 224 | camera_ready.set() 225 | 226 | t.join() 227 | 228 | cef.QuitMessageLoop() 229 | browser.CloseBrowser() 230 | cef.Shutdown() 231 | 232 | except: 233 | print('Interrupted') 234 | run_event.clear() 235 | t.join() 236 | 237 | cef.QuitMessageLoop() 238 | browser.CloseBrowser() 239 | cef.Shutdown() 240 | try: 241 | sys.exit(0) 242 | except SystemExit: 243 | os._exit(0) 244 | -------------------------------------------------------------------------------- /libs/Processing.NDI.Lib.Licenses.txt: -------------------------------------------------------------------------------- 1 | ******************************************************************************************************** 2 | NewTek NDI 3 | Copyright(c) 2014-2020, NewTek, inc. 4 | ******************************************************************************************************** 5 | 6 | The NDI SDK license available at: 7 | http://new.tk/ndisdk_license/ 8 | 9 | The NDI Embedded SDK license is available at 10 | http://new.tk/ndisdk_embedded_license/ 11 | 12 | For more information about NDI please visit: 13 | http://ndi.tv/ 14 | 15 | This file should be included with all distribution of the binary files included with the NDI SDK. 16 | 17 | We are truly thankful for the NDI community and the amazing support that it has shown us over the years. 18 | 19 | ******************************************************************************************************** 20 | NDI gratefully uses the following third party libraries. 21 | 22 | ******************************************************************************************************** 23 | RAPIDJSON (https://rapidjson.org/) 24 | 25 | Tencent is pleased to support the open source community by making RapidJSON available. 26 | 27 | Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. 28 | 29 | Licensed under the MIT License (the "License"); you may not use this file except 30 | in compliance with the License. You may obtain a copy of the License at 31 | 32 | http://opensource.org/licenses/MIT 33 | 34 | Unless required by applicable law or agreed to in writing, software distributed 35 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 36 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 37 | specific language governing permissions and limitations under the License. 38 | 39 | **************************************************** 40 | SPEEX (https://www.speex.org/), resampling code only. 41 | 42 | © 2002-2003, Jean-Marc Valin/Xiph.Org Foundation 43 | 44 | Redistribution and use in source and binary forms, with or without modification, are permitted provided 45 | that the following conditions are met: 46 | 47 | Redistributions of source code must retain the above copyright notice, this list of conditions and the 48 | following disclaimer. 49 | 50 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 51 | following disclaimer in the documentation and/or other materials provided with the distribution. 52 | Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or 53 | promote products derived from this software without specific prior written permission. 54 | 55 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, 56 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 57 | ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 58 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 59 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 60 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 61 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 62 | 63 | ******************************************************************************************************** 64 | RapidXML (http://rapidxml.sourceforge.net/) 65 | 66 | Copyright (c) 2006, 2007 Marcin Kalicinski 67 | 68 | Permission is hereby granted, free of charge, to any person or organization 69 | obtaining a copy of the software and accompanying documentation covered by 70 | this license (the "Software") to use, reproduce, display, distribute, 71 | execute, and transmit the Software, and to prepare derivative works of the 72 | Software, and to permit third-parties to whom the Software is furnished to 73 | do so, all subject to the following: 74 | 75 | The copyright notices in the Software and this entire statement, including 76 | the above license grant, this restriction and the following disclaimer, 77 | must be included in all copies of the Software, in whole or in part, and 78 | all derivative works of the Software, unless such copies or derivative 79 | works are solely in the form of machine-executable object code generated by 80 | a source language processor. 81 | 82 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 83 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 84 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 85 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 86 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 87 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 88 | DEALINGS IN THE SOFTWARE. 89 | 90 | ******************************************************************************************************** 91 | CxxUrl (https://github.com/chmike/CxxUrl) 92 | 93 | The MIT License (MIT) 94 | 95 | Copyright (c) 2015 Christophe Meessen 96 | 97 | Permission is hereby granted, free of charge, to any person obtaining a copy 98 | of this software and associated documentation files (the "Software"), to deal 99 | in the Software without restriction, including without limitation the rights 100 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 101 | copies of the Software, and to permit persons to whom the Software is 102 | furnished to do so, subject to the following conditions: 103 | 104 | The above copyright notice and this permission notice shall be included in all 105 | copies or substantial portions of the Software. 106 | 107 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 108 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 109 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 110 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 111 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 112 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 113 | SOFTWARE. 114 | 115 | ******************************************************************************************************** 116 | ASIO (non Boost version) https://think-async.com/Asio/ 117 | 118 | Boost Software License - Version 1.0 - August 17th, 2003 119 | 120 | Permission is hereby granted, free of charge, to any person or organization 121 | obtaining a copy of the software and accompanying documentation covered by 122 | this license (the "Software") to use, reproduce, display, distribute, 123 | execute, and transmit the Software, and to prepare derivative works of the 124 | Software, and to permit third-parties to whom the Software is furnished to 125 | do so, all subject to the following: 126 | 127 | The copyright notices in the Software and this entire statement, including 128 | the above license grant, this restriction and the following disclaimer, 129 | must be included in all copies of the Software, in whole or in part, and 130 | all derivative works of the Software, unless such copies or derivative 131 | works are solely in the form of machine-executable object code generated by 132 | a source language processor. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 135 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 136 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 137 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 138 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 139 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 140 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /libs/hook-cefpython3.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is PyInstaller hook file for CEF Python. This file 3 | helps PyInstaller find CEF Python dependencies that are 4 | required to run final executable. 5 | 6 | See PyInstaller docs for hooks: 7 | https://pyinstaller.readthedocs.io/en/stable/hooks.html 8 | """ 9 | ## this file was modified by steve, but it's not originally by steve 10 | 11 | import glob 12 | import os 13 | import platform 14 | import re 15 | import sys 16 | import PyInstaller 17 | from PyInstaller.utils.hooks import is_module_satisfies, get_package_paths 18 | from PyInstaller.compat import is_win, is_darwin, is_linux, is_py2 19 | from PyInstaller import log as logging 20 | 21 | # Constants 22 | CEFPYTHON_MIN_VERSION = "57.0" 23 | PYINSTALLER_MIN_VERSION = "3.2.1" 24 | 25 | # Makes assumption that using "python.exe" and not "pyinstaller.exe" 26 | # TODO: use this code to work cross-platform: 27 | # > from PyInstaller.utils.hooks import get_package_paths 28 | # > get_package_paths("cefpython3") 29 | 30 | CEFPYTHON3_DIR = get_package_paths("cefpython3")[1] 31 | 32 | CYTHON_MODULE_EXT = ".pyd" if is_win else ".so" 33 | 34 | # Globals 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | # Functions 39 | def check_platforms(): 40 | if not is_win and not is_darwin and not is_linux: 41 | raise SystemExit("Error: Currently only Windows, Linux and Darwin " 42 | "platforms are supported, see Issue #135.") 43 | 44 | 45 | def check_pyinstaller_version(): 46 | """Using is_module_satisfies() for pyinstaller fails when 47 | installed using 'pip install develop.zip' command 48 | (PyInstaller Issue #2802).""" 49 | # Example version string for dev version of pyinstaller: 50 | # > 3.3.dev0+g5dc9557c 51 | version = PyInstaller.__version__ 52 | match = re.search(r"^\d+\.\d+(\.\d+)?", version) 53 | if not (match.group(0) >= PYINSTALLER_MIN_VERSION): 54 | raise SystemExit("Error: pyinstaller %s or higher is required" 55 | % PYINSTALLER_MIN_VERSION) 56 | 57 | 58 | def check_cefpython3_version(): 59 | if not is_module_satisfies("cefpython3 >= %s" % CEFPYTHON_MIN_VERSION): 60 | raise SystemExit("Error: cefpython3 %s or higher is required" 61 | % CEFPYTHON_MIN_VERSION) 62 | 63 | 64 | def get_cefpython_modules(): 65 | """Get all cefpython Cython modules in the cefpython3 package. 66 | It returns a list of names without file extension. Eg. 67 | 'cefpython_py27'. """ 68 | pyds = glob.glob(os.path.join(CEFPYTHON3_DIR, 69 | "cefpython_py*" + CYTHON_MODULE_EXT)) 70 | assert len(pyds) > 1, "Missing cefpython3 Cython modules" 71 | modules = [] 72 | for path in pyds: 73 | filename = os.path.basename(path) 74 | mod = filename.replace(CYTHON_MODULE_EXT, "") 75 | modules.append(mod) 76 | return modules 77 | 78 | 79 | def get_excluded_cefpython_modules(): 80 | """CEF Python package includes Cython modules for various Python 81 | versions. When using Python 2.7 pyinstaller should not 82 | bundle modules for eg. Python 3.6, otherwise it will 83 | cause to include Python 3 dll dependencies. Returns a list 84 | of fully qualified names eg. 'cefpython3.cefpython_py27'.""" 85 | pyver = "".join(map(str, sys.version_info[:2])) 86 | pyver_string = "py%s" % pyver 87 | modules = get_cefpython_modules() 88 | excluded = [] 89 | for mod in modules: 90 | if pyver_string in mod: 91 | continue 92 | excluded.append("cefpython3.%s" % mod) 93 | logger.info("Exclude cefpython3 module: %s" % excluded[-1]) 94 | return excluded 95 | 96 | 97 | def get_cefpython3_datas(): 98 | """Returning almost all of cefpython binaries as DATAS (see exception 99 | below), because pyinstaller does strange things and fails if these are 100 | returned as BINARIES. It first updates manifest in .dll files: 101 | >> Updating manifest in chrome_elf.dll 102 | 103 | And then because of that it fails to load the library: 104 | >> hsrc = win32api.LoadLibraryEx(filename, 0, LOAD_LIBRARY_AS_DATAFILE) 105 | >> pywintypes.error: (5, 'LoadLibraryEx', 'Access is denied.') 106 | 107 | It is not required for pyinstaller to modify in any way 108 | CEF binaries or to look for its dependencies. CEF binaries 109 | does not have any external dependencies like MSVCR or similar. 110 | 111 | The .pak .dat and .bin files cannot be marked as BINARIES 112 | as pyinstaller would fail to find binary depdendencies on 113 | these files. 114 | 115 | One exception is subprocess (subprocess.exe on Windows) executable 116 | file, which is passed to pyinstaller as BINARIES in order to collect 117 | its dependecies. 118 | 119 | DATAS are in format: tuple(full_path, dest_subdir). 120 | """ 121 | ret = list() 122 | 123 | if is_win: 124 | cefdatadir = "." 125 | elif is_darwin or is_linux: 126 | cefdatadir = "." 127 | else: 128 | assert False, "Unsupported system {}".format(platform.system()) 129 | 130 | # Binaries, licenses and readmes in the cefpython3/ directory 131 | for filename in os.listdir(CEFPYTHON3_DIR): 132 | # Ignore Cython modules which are already handled by 133 | # pyinstaller automatically. 134 | if filename[:-len(CYTHON_MODULE_EXT)] in get_cefpython_modules(): 135 | continue 136 | 137 | # CEF binaries and datas 138 | extension = os.path.splitext(filename)[1] 139 | if extension in \ 140 | [".exe", ".dll", ".pak", ".dat", ".bin", ".txt", ".so", ".plist"] \ 141 | or filename.lower().startswith("license"): 142 | logger.info("Include cefpython3 data: {}".format(filename)) 143 | ret.append((os.path.join(CEFPYTHON3_DIR, filename), cefdatadir)) 144 | 145 | if is_darwin: 146 | # "Chromium Embedded Framework.framework/Resources" with subdirectories 147 | # is required. Contain .pak files and locales (each locale in separate 148 | # subdirectory). 149 | resources_subdir = \ 150 | os.path.join("Chromium Embedded Framework.framework", "Resources") 151 | base_path = os.path.join(CEFPYTHON3_DIR, resources_subdir) 152 | assert os.path.exists(base_path), \ 153 | "{} dir not found in cefpython3".format(resources_subdir) 154 | for path, dirs, files in os.walk(base_path): 155 | for file in files: 156 | absolute_file_path = os.path.join(path, file) 157 | dest_path = os.path.relpath(path, CEFPYTHON3_DIR) 158 | ret.append((absolute_file_path, dest_path)) 159 | logger.info("Include cefpython3 data: {}".format(dest_path)) 160 | elif is_win or is_linux: 161 | # The .pak files in cefpython3/locales/ directory 162 | locales_dir = os.path.join(CEFPYTHON3_DIR, "locales") 163 | assert os.path.exists(locales_dir), \ 164 | "locales/ dir not found in cefpython3" 165 | for filename in os.listdir(locales_dir): 166 | logger.info("Include cefpython3 data: {}/{}".format( 167 | os.path.basename(locales_dir), filename)) 168 | ret.append((os.path.join(locales_dir, filename), 169 | os.path.join(cefdatadir, "locales"))) 170 | 171 | # Optional .so/.dll files in cefpython3/swiftshader/ directory 172 | swiftshader_dir = os.path.join(CEFPYTHON3_DIR, "swiftshader") 173 | if os.path.isdir(swiftshader_dir): 174 | for filename in os.listdir(swiftshader_dir): 175 | logger.info("Include cefpython3 data: {}/{}".format( 176 | os.path.basename(swiftshader_dir), filename)) 177 | ret.append((os.path.join(swiftshader_dir, filename), 178 | os.path.join(cefdatadir, "swiftshader"))) 179 | return ret 180 | 181 | 182 | # ---------------------------------------------------------------------------- 183 | # Main 184 | # ---------------------------------------------------------------------------- 185 | 186 | # Checks 187 | check_platforms() 188 | check_pyinstaller_version() 189 | check_cefpython3_version() 190 | 191 | # Info 192 | logger.info("CEF Python package directory: %s" % CEFPYTHON3_DIR) 193 | 194 | # Hidden imports. 195 | # PyInstaller has no way on detecting imports made by Cython 196 | # modules, so all pure Python imports made in cefpython .pyx 197 | # files need to be manually entered here. 198 | # TODO: Write a tool script that would find such imports in 199 | # .pyx files automatically. 200 | hiddenimports = [ 201 | "codecs", 202 | "copy", 203 | "datetime", 204 | "inspect", 205 | "json", 206 | "os", 207 | "platform", 208 | "random", 209 | "re", 210 | "sys", 211 | "time", 212 | "traceback", 213 | "types", 214 | "urllib", 215 | "weakref", 216 | ] 217 | if is_py2: 218 | hiddenimports += [ 219 | "urlparse", 220 | ] 221 | 222 | # Excluded modules 223 | excludedimports = get_excluded_cefpython_modules() 224 | 225 | # Include binaries requiring to collect its dependencies 226 | if is_darwin or is_linux: 227 | binaries = [(os.path.join(CEFPYTHON3_DIR, "subprocess"), ".")] 228 | elif is_win: 229 | binaries = [(os.path.join(CEFPYTHON3_DIR, "subprocess.exe"), ".")] 230 | else: 231 | binaries = [] 232 | 233 | # Include datas 234 | datas = get_cefpython3_datas() 235 | 236 | # Notify pyinstaller.spec code that this hook was executed 237 | # and that it succeeded. 238 | os.environ["PYINSTALLER_CEFPYTHON3_HOOK_SUCCEEDED"] = "1" 239 | --------------------------------------------------------------------------------