├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── not-casting-working-after-fresh-install-update.md │ ├── particular-video-file-not-working.md │ ├── problems-with-video-transcoding.md │ └── translation-request.md ├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── _config.yml ├── addons.js ├── appIcon ├── cast-to-tv.svg └── prefs.png ├── compat.js ├── crowdin.yml ├── extension.js ├── file-chooser.js ├── helper.js ├── metadata.json ├── nautilus └── nautilus-cast-to-tv.py ├── node_scripts ├── addons-importer.js ├── bridge.js ├── chromecast.js ├── encode.js ├── events.js ├── gettext.js ├── gnome.js ├── messages.js ├── notify.js ├── playercast.js ├── remote-controller.js ├── remove.js ├── sender.js ├── server-socket.js ├── server.js ├── utils │ ├── install.js │ ├── local-ip.js │ ├── scanner.js │ └── vttextract.js └── web-creator.js ├── package-lock.json ├── package.json ├── playlist.js ├── po ├── cast-to-tv-links-addon │ ├── cast-to-tv-links-addon.pot │ ├── de.po │ ├── en_GB.po │ ├── es.po │ ├── fo.po │ ├── fr.po │ ├── it.po │ ├── nl.po │ ├── pl.po │ ├── ro.po │ └── tr.po └── cast-to-tv │ ├── cast-to-tv.pot │ ├── de.po │ ├── en_GB.po │ ├── es.po │ ├── fo.po │ ├── fr.po │ ├── it.po │ ├── nl.po │ ├── pl.po │ ├── ro.po │ └── tr.po ├── prefs.js ├── prefs_shared.js ├── schemas ├── gschemas.compiled └── org.gnome.shell.extensions.cast-to-tv.gschema.xml ├── server-monitor.js ├── shared.js ├── soup.js ├── temp.js ├── webplayer ├── images │ ├── cover.png │ ├── icon.png │ └── play.png ├── loading.html ├── loading.js ├── message.html ├── message.js ├── picture.html ├── preload.js ├── webplayer-defaults.js ├── webplayer-encode.js ├── webplayer-init.js ├── webplayer.css ├── webplayer_direct.html ├── webplayer_encode.html └── websocket.js └── widget.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/Rafostar 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/not-casting-working-after-fresh-install-update.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Not casting/working after fresh install/update 3 | about: If you just installed or updated this extension follow these steps. 4 | title: Casting not working 5 | labels: '' 6 | assignees: Rafostar 7 | 8 | --- 9 | 10 | 1. After a fresh install of this extension and it's node modules from extension prefs, usually a reboot fixes this issue (and by reboot I mean full system reboot - not just restarting gnome-shell). 11 | 12 | 2. Make sure you have receiver type in extension prefs set correctly to desired device type (e.g. "Chromecast"). If it wasn't try casting again. 13 | 14 | 3. If extension seems to be working (you have a top panel drop down menu), but still experiencing problems with casting, lets take a look at what happens during file cast. Turn off this extension node service without disabling whole extension (from gnome top bar menu). TV indicator icon should disappear from top bar. Then do the following in terminal: 15 | 16 | ``` 17 | cd ~/.local/share/gnome-shell/extensions/cast-to-tv@rafostar.github.com/node_scripts 18 | DEBUG=bridge,chromecast* node server 19 | ``` 20 | 21 | After running, extension menu and indicator should reappear. 22 | Try reproducing your problem and send me output. Stop the terminal process with Ctrl+c buttons. 23 | 24 | Output: 25 | ``` 26 | COPY_HERE 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/particular-video-file-not-working.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Particular video file not working 3 | about: Problems with a certain video file, while others work fine. 4 | title: Video file not working 5 | labels: '' 6 | assignees: Rafostar 7 | 8 | --- 9 | 10 | Before submitting this issue, please try casting with transcoding audio, video and video+audio (you can find selection box on bottom-left of select video window or in Nautilus right click menu), if neither of them solves your problem fill and submit this new issue. 11 | 12 | Please send me the output of (replace `VIDEO.mkv` with your actual file name): 13 | ``` 14 | ffprobe -show_streams -show_format -print_format json "VIDEO.mkv" 15 | ``` 16 | 17 | Output: 18 | ``` 19 | COPY_HERE 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/problems-with-video-transcoding.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Problems with video transcoding 3 | about: Video transcoding (either software or hardware accelerated) not working. 4 | title: Transcoding not working 5 | labels: '' 6 | assignees: Rafostar 7 | 8 | --- 9 | 10 | CPU: e.g. i9-9900k 11 | GPU: e.g. Intel UHD Graphics 630 12 | 13 | Turn off this extension node service without disabling whole extension (from gnome top bar menu). TV indicator icon should disappear from top bar. Then do the following in terminal: 14 | ``` 15 | cd ~/.local/share/gnome-shell/extensions/cast-to-tv@rafostar.github.com/node_scripts 16 | DEBUG=bridge,ffmpeg node server 17 | ``` 18 | After running, extension menu and indicator should reappear. 19 | Try reproducing your problem and send me output. Stop the terminal process with Ctrl+c buttons. 20 | 21 | Output: 22 | ``` 23 | COPY_HERE 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translation-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Translation request 3 | about: Request adding new language 4 | title: Translation request 5 | labels: translation 6 | assignees: Rafostar 7 | 8 | --- 9 | 10 |

11 | 12 | Cast to TV - Crowdin 13 | 14 |

15 | 16 | If you want to become a translator for missing language, fill the below template (I must add new language to Crowdin first). 17 | 18 | Using Crowdin for updating translations is preferred, otherwise please use an editing app like `poedit` for generating files and not do it in plain text editor. 19 | 20 | --- 21 | I am willing to translate the extension into language: _____ 22 | 23 | Select method: 24 | - [ ] I will use Crowdin (preferred method). Please add my language to the project. 25 | - [ ] I will translate on my own using translation software (e.g. `poedit`). 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | locale/ 3 | locale_addons/ 4 | po/cast-to-tv*/*.po~ 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Basic Makefile 2 | 3 | EXTNAME = gnome-shell-extension-cast-to-tv 4 | UUID = cast-to-tv@rafostar.github.com 5 | GETTEXT = cast-to-tv 6 | PACKAGE = "Cast to TV" 7 | TOLOCALIZE = extension.js widget.js playlist.js file-chooser.js prefs.js node_scripts/messages.js 8 | MSGSRC = $(wildcard ./po/cast-to-tv/*.po) 9 | POTFILE = ./po/cast-to-tv/cast-to-tv.pot 10 | ZIPFILES = *.js *.json node_scripts webplayer schemas locale appIcon nautilus COPYING README.md 11 | INSTALLPATH = ~/.local/share/gnome-shell/extensions 12 | 13 | # Add-ons translations # 14 | POFOLDERS = $(wildcard ./po/cast-to-tv-*-addon) 15 | 16 | # Compile schemas # 17 | glib-schemas: 18 | glib-compile-schemas ./schemas/ 19 | 20 | # Create/update potfile # 21 | potfile: 22 | mkdir -p ./po 23 | xgettext -o $(POTFILE) --language=JavaScript --add-comments=TRANSLATORS: --package-name $(PACKAGE) $(TOLOCALIZE) 24 | 25 | # Update '.po' from 'potfile' # 26 | mergepo: 27 | for i in $(MSGSRC); do \ 28 | msgmerge -U $$i $(POTFILE); \ 29 | done; 30 | 31 | # Compile all .mo files # 32 | compilemo: compilemo-base compilemo-addons 33 | 34 | # Compile extension .mo files # 35 | compilemo-base: 36 | for i in $(MSGSRC); do \ 37 | msggrep $$i -T -e "" | grep -q "" \ 38 | && mkdir -p ./locale/`basename $$i .po`/LC_MESSAGES \ 39 | && msgfmt -o ./locale/`basename $$i .po`/LC_MESSAGES/$(GETTEXT).mo $$i; \ 40 | done; 41 | 42 | # Compile addons .mo files # 43 | compilemo-addons: 44 | for i in $(POFOLDERS); do \ 45 | for j in $$i/*.po; do \ 46 | msggrep $$j -T -e "" | grep -q "" \ 47 | && mkdir -p ./locale_addons/`basename $$i`/`basename $$j .po`/LC_MESSAGES \ 48 | && msgfmt -o ./locale_addons/`basename $$i`/`basename $$j .po`/LC_MESSAGES/`basename $$i`.mo $$j; \ 49 | done; \ 50 | done; 51 | 52 | # Create release zip # 53 | zip-file: _build 54 | zip -qr $(UUID).zip $(ZIPFILES) 55 | 56 | # Update metadata # 57 | metadata: 58 | ifeq ($(CUSTOMPATH),) 59 | ifeq ($(PKGDIR),) 60 | LASTCOMMIT=$(shell git rev-parse --short HEAD); \ 61 | grep -q '"git":' metadata.json \ 62 | && sed -i "/\"git\":/c \ \ \"git\": \"$$LASTCOMMIT\"," metadata.json \ 63 | || sed -i "/uuid/a \ \ \"git\": \"$$LASTCOMMIT\"," metadata.json 64 | else 65 | grep -q '"custom-install":' metadata.json \ 66 | || sed -i "/uuid/a \ \ \"custom-install\": true," metadata.json 67 | endif 68 | endif 69 | 70 | # Build and install # 71 | install: compilemo-base metadata 72 | ifeq ($(CUSTOMPATH),) 73 | glib-compile-schemas ./schemas/ 74 | mkdir -p $(INSTALLPATH)/$(UUID) 75 | cp -r $(ZIPFILES) $(INSTALLPATH)/$(UUID) 76 | else 77 | mkdir -p $(CUSTOMPATH)/$(UUID) 78 | cp -r $(filter-out schemas locale README.md COPYING, $(ZIPFILES)) $(CUSTOMPATH)/$(UUID) 79 | mkdir -p $(PKGDIR)/usr/share/glib-2.0/schemas 80 | cp -r ./schemas/*.gschema.* $(PKGDIR)/usr/share/glib-2.0/schemas/ 81 | glib-compile-schemas $(PKGDIR)/usr/share/glib-2.0/schemas 2>/dev/null 82 | mkdir -p $(PKGDIR)/usr/share/locale 83 | cp -r ./locale/* $(PKGDIR)/usr/share/locale/ 84 | mkdir -p $(PKGDIR)/usr/share/doc/$(EXTNAME) 85 | cp ./README.md $(PKGDIR)/usr/share/doc/$(EXTNAME)/ 86 | mkdir -p $(PKGDIR)/usr/share/licenses/$(EXTNAME) 87 | cp ./COPYING $(PKGDIR)/usr/share/licenses/$(EXTNAME)/ 88 | mkdir -p $(CUSTOMPATH)/$(UUID)/node_modules 89 | chmod 777 $(CUSTOMPATH)/$(UUID)/node_modules 90 | endif 91 | 92 | _build: glib-schemas compilemo-base 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gnome Shell Extension Cast to TV 2 | [![License](https://img.shields.io/github/license/Rafostar/gnome-shell-extension-cast-to-tv.svg)](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/blob/master/COPYING) 3 | [![Crowdin](https://d322cqt584bo4o.cloudfront.net/cast-to-tv/localized.svg)](https://crowdin.com/project/cast-to-tv) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TFVDFD88KQ322) 5 | [![Donate](https://img.shields.io/badge/Donate-PayPal.Me-lightgrey.svg)](https://www.paypal.me/Rafostar) 6 | [![Twitter](https://img.shields.io/twitter/url/https/github.com/Rafostar/gnome-shell-extension-cast-to-tv.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2FRafostar%2Fgnome-shell-extension-cast-to-tv) 7 | 8 |

9 | 10 |

11 | 12 | ## Features 13 | * Cast videos, music and pictures to: 14 | * Chromecast devices 15 | * Any device with web browser (other PC or smartphone) 16 | * Media player app (eg. MPV, VLC) 17 | * Supports external and built-in subtitles (along with custom fansubs) 18 | * Chromecast remote controller (control playback from gnome top bar) 19 | * Play on other device using integrated web player and change content without refreshing web page 20 | * Transcode videos to supported format on the fly 21 | * Optional VAAPI/NVENC video encoding for low cpu usage 22 | * Stream music with visualizations (requires fast cpu) 23 | * Nautilus right click menu integration 24 | * Media playlist with "Drag and Drop" support 25 | 26 | [Playercast](https://rafostar.github.io/playercast) app turns your media player on any other Linux device (e.g. HTPC, Raspberry Pi) into a media receiver that works similarly to Chromecast. 27 | 28 | Expand extension functionality through Add-ons: 29 | * [Links Add-on](https://github.com/Rafostar/cast-to-tv-links-addon) - cast media from web pages 30 | * [Desktop Add-on](https://github.com/Rafostar/cast-to-tv-desktop-addon) - desktop streaming 31 | 32 | ## Download 33 | ### For latest release and changelog check out [releases page](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/releases). 34 | 35 | [](https://extensions.gnome.org/extension/1544/cast-to-tv) 36 | 37 | Installation from source code is described in the [wiki](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/wiki). 38 | 39 | After enabling the extension, remember to install all [requirements](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv#requirements) and [npm dependencies](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv#install-npm-dependencies). 40 | 41 | ## Requirements 42 | Here is a list of required programs that Cast to TV depends on: 43 | * [npm](https://www.npmjs.com/get-npm) (for dependencies installation) 44 | * [nodejs](https://nodejs.org) (v8.6 or newer) 45 | * [ffmpeg](https://ffmpeg.org) (with ffprobe) 46 | 47 | Please make sure you have all of the above installed. 48 | 49 | ### Optional: 50 | * [nautilus-python](https://github.com/GNOME/nautilus-python) (for nautilus integration) 51 | 52 | Nautilus extension is included in Cast to TV (since version 9). 53 | 54 | You can optionally use hardware VAAPI or NVENC encoding. This of course requires working drivers. More info and how to install hardware acceleration [here](https://wiki.archlinux.org/index.php/Hardware_video_acceleration). 55 | 56 | ## Installation 57 | 58 | ### Ubuntu 59 | Having enabled universe repo run: 60 | ``` 61 | sudo apt install npm nodejs ffmpeg 62 | ``` 63 | Ubuntu is shipping wrong npm version for some reason.
64 | Update it and clear bash cache: 65 | ``` 66 | sudo npm install -g npm 67 | hash -r 68 | ``` 69 | 70 | ### Fedora 71 | Having enabled rpm fusion repos run: 72 | ``` 73 | sudo dnf install npm nodejs ffmpeg 74 | ``` 75 | 76 | ### Arch 77 | ``` 78 | sudo pacman -S npm nodejs ffmpeg 79 | ``` 80 | 81 | ### Nautilus integration (optional) 82 | * Ubuntu: `sudo apt install python3-nautilus python3-gi` 83 | * Fedora: `sudo dnf install nautilus-python python3-gobject` 84 | * Arch: `sudo pacman -S python-nautilus python-gobject` 85 | 86 | Ubuntu releases older than 20.04 require `python-nautilus` instead of ` python3-nautilus`.
87 | Older Fedora releases additionally require `pygobject3`. 88 | 89 | Nautilus integration is disabled by default. Remember to turn it on in this extension settings. 90 | 91 | ## Install npm dependencies 92 | **Before using extension** you also **must** install some additional npm packages. 93 | 94 | You should also repeat this step when updating the extension to the new version, otherwise you may not have newly added or updated dependencies. 95 | 96 | ### New method 97 | In version 9 and later this can be done from extension preferences.
98 | Go to `Cast Settings -> Modules` and click `Install npm modules` button. 99 | 100 | You must have `npm` and `nodejs` installed prior to this step. 101 | 102 | ### Old method 103 | Run below code in terminal: 104 | ``` 105 | cd ~/.local/share/gnome-shell/extensions/cast-to-tv@rafostar.github.com 106 | npm install 107 | ``` 108 | 109 | ## How to use 110 | Detailed instructions related to configuration and using the extension are in the [wiki](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/wiki).
111 | You can also find some usage examples and firewall config there. 112 | 113 | Check out [FAQ](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/wiki/FAQ), before asking questions. 114 | 115 | ## Info for translators 116 | Preferred translation method is to use [Cast to TV Crowdin](https://crowdin.com/project/cast-to-tv) web page. 117 | 118 | Crowdin does not require any additional tools and translating can be done through web browser. You can login using GitHub account or create a new one. Only I can add new languages to this project, so if your language is not available, please contact me first (you can leave comment [here](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/issues/29)). 119 | 120 | Alternatively you can still use Makefile and tools like Poedit to generate translations: 121 | 122 | `make potfile` - generates updated POT file.
123 | `make mergepo` - merges changes from POT file into all PO files.
124 | `make compilemo` - compiles translation files.
125 | 126 | After compiling restart gnome-shell for changes to be applied. 127 | 128 | ## Special Thanks 129 | Special thanks go to [Simon Kusterer (xat)](https://github.com/xat) for developing [chromecast-player](https://github.com/xat/chromecast-player) and [Sam Potts](https://github.com/sampotts) for making [Plyr](https://github.com/sampotts/plyr), an awesome HTML5 video player. 130 | 131 | ### Nautilus Extension 132 | Many thanks to [Rendy Anthony](https://github.com/rendyanthony) for helping me make Nautilus integration based on his [nautilus-cast](https://github.com/rendyanthony/nautilus-cast) extension. 133 | 134 | ### Translations 135 | Many thanks to everyone involved in translating this extension either through GitHub or Crowdin. 136 | 137 | ## Donation 138 | If you like my work please support it by buying me a cup of coffee :-) 139 | 140 | [![PayPal](https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/wiki/images/paypal.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TFVDFD88KQ322) 141 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /addons.js: -------------------------------------------------------------------------------- 1 | const { GLib } = imports.gi; 2 | const Main = imports.ui.main; 3 | const AggregateMenu = Main.panel.statusArea.aggregateMenu; 4 | 5 | let castMenu; 6 | let addonMenuItems = {}; 7 | let addonMenuSignals = {}; 8 | let timeouts = {}; 9 | 10 | function findCastToTv() 11 | { 12 | let menuItems = AggregateMenu.menu._getMenuItems(); 13 | let index = 0; 14 | 15 | while(index < menuItems.length) 16 | { 17 | if( 18 | menuItems[index].hasOwnProperty('extensionId') 19 | && menuItems[index].extensionId === 'cast-to-tv' 20 | ) { 21 | return menuItems[index]; 22 | } 23 | 24 | index++; 25 | } 26 | 27 | return null; 28 | } 29 | 30 | function setLastMenuItem(extMenu, item, endOffset) 31 | { 32 | if(!endOffset) endOffset = 0; 33 | 34 | let subMenuItems = extMenu.castSubMenu.menu._getMenuItems(); 35 | let lastItemIndex = subMenuItems.length - 1; 36 | extMenu.castSubMenu.menu.moveMenuItem(item, lastItemIndex - endOffset); 37 | } 38 | 39 | function enableAddon(uuid) 40 | { 41 | let addonName = uuid.split('@')[0]; 42 | 43 | if(timeouts[addonName]) 44 | return; 45 | 46 | /* Give main extension time to finish startup */ 47 | timeouts[addonName] = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2500, () => 48 | { 49 | timeouts[addonName] = null; 50 | 51 | castMenu = findCastToTv(); 52 | let mainExtension = Main.extensionManager.lookup('cast-to-tv@rafostar.github.com'); 53 | let addonExtension = Main.extensionManager.lookup(uuid); 54 | 55 | if(!castMenu || !mainExtension || !addonExtension) 56 | return GLib.SOURCE_REMOVE; 57 | 58 | let { helper, soup, shared } = mainExtension.imports; 59 | let { AddonMenuItem } = addonExtension.imports.widget; 60 | 61 | addonMenuItems[addonName] = new AddonMenuItem({ 62 | helper: helper, 63 | soupClient: soup.client, 64 | shared: shared.module.exports, 65 | path: mainExtension.path 66 | }); 67 | 68 | addonMenuSignals[addonName] = [ 69 | addonMenuItems[addonName].connect('destroy', () => 70 | { 71 | addonMenuSignals[addonName].forEach(signal => 72 | addonMenuItems[addonName].disconnect(signal)) 73 | 74 | addonMenuSignals[addonName] = null; 75 | addonMenuItems[addonName].destroyed = true; 76 | }) 77 | ]; 78 | 79 | if( 80 | addonMenuItems[addonName].hasOwnProperty('hasExtApp') 81 | && addonMenuItems[addonName].hasExtApp 82 | ) { 83 | addonMenuSignals[addonName].push( 84 | addonMenuItems[addonName].connect('activate', () => 85 | helper.startApp(addonExtension.path) 86 | ) 87 | ) 88 | } 89 | else 90 | { 91 | addonMenuSignals[addonName].push( 92 | addonMenuItems[addonName].connect('activate', () => 93 | helper.closeOtherApps() 94 | ) 95 | ) 96 | } 97 | 98 | let castMenuItems = castMenu.castSubMenu.menu._getMenuItems(); 99 | let insertIndex = castMenuItems.length - 1; 100 | 101 | let prevMenuItem = castMenuItems.find(item => 102 | { 103 | if( 104 | item.hasOwnProperty('isDesktopStream') 105 | || (castMenuItems.indexOf(item) > 2 106 | && addonMenuItems[addonName].label.text < item.label.text) 107 | ) { 108 | return true; 109 | } 110 | 111 | return false; 112 | }); 113 | 114 | /* Desktop streaming should be last on the list (experimental feature) */ 115 | if(prevMenuItem && !addonMenuItems[addonName].hasOwnProperty('isDesktopStream')) 116 | insertIndex = castMenuItems.indexOf(prevMenuItem); 117 | 118 | castMenu.castSubMenu.menu.addMenuItem(addonMenuItems[addonName], insertIndex); 119 | 120 | if( 121 | castMenu.hasOwnProperty('isServiceEnabled') 122 | && castMenu.isServiceEnabled === false 123 | ) { 124 | if(addonMenuItems[addonName].hasOwnProperty('actor')) 125 | addonMenuItems[addonName].actor.hide(); 126 | else 127 | addonMenuItems[addonName].hide(); 128 | } 129 | 130 | if(castMenu.hasOwnProperty('serviceMenuItem')) 131 | setLastMenuItem(castMenu, castMenu.serviceMenuItem); 132 | 133 | if(castMenu.hasOwnProperty('settingsMenuItem')) 134 | setLastMenuItem(castMenu, castMenu.settingsMenuItem); 135 | 136 | return GLib.SOURCE_REMOVE; 137 | }); 138 | } 139 | 140 | function disableAddon(uuid) 141 | { 142 | let addonName = uuid.split('@')[0]; 143 | 144 | if(timeouts[addonName]) 145 | { 146 | GLib.source_remove(timeouts[addonName]); 147 | timeouts[addonName] = null; 148 | } 149 | 150 | if(!addonMenuItems[addonName] || addonMenuItems[addonName].destroyed) 151 | return; 152 | 153 | /* No need to reorder menu items when locking screen, 154 | as whole cast menu will be destroyed then anyway */ 155 | let lockingScreen = ( 156 | Main.sessionMode.currentMode == 'unlock-dialog' 157 | || Main.sessionMode.currentMode == 'lock-screen' 158 | ); 159 | 160 | if(!lockingScreen && castMenu) 161 | setLastMenuItem(castMenu, addonMenuItems[addonName]); 162 | 163 | /* Force GUI refresh by hiding item before removal */ 164 | if(addonMenuItems[addonName].hasOwnProperty('actor')) 165 | addonMenuItems[addonName].actor.hide(); 166 | else 167 | addonMenuItems[addonName].hide(); 168 | 169 | addonMenuItems[addonName].destroy(); 170 | addonMenuItems[addonName] = null; 171 | } 172 | -------------------------------------------------------------------------------- /appIcon/cast-to-tv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 | -------------------------------------------------------------------------------- /appIcon/prefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rafostar/gnome-shell-extension-cast-to-tv/10502fc3cd247f776895d418585243477de23e24/appIcon/prefs.png -------------------------------------------------------------------------------- /compat.js: -------------------------------------------------------------------------------- 1 | const { Clutter, GObject } = imports.gi; 2 | const PopupMenu = imports.ui.popupMenu; 3 | const Config = imports.misc.config; 4 | 5 | var IS_OLD_SHELL = (Config.PACKAGE_VERSION.split('.')[1] < 35); 6 | 7 | var AltPopupBase = GObject.registerClass( 8 | class AltPopupBase extends PopupMenu.PopupBaseMenuItem 9 | { 10 | _init() 11 | { 12 | super._init({ hover: false }); 13 | } 14 | 15 | add_style_pseudo_class() 16 | { 17 | return null; 18 | } 19 | 20 | _onButtonReleaseEvent(actor, event) 21 | { 22 | return this.vfunc_button_release_event(); 23 | } 24 | 25 | vfunc_button_release_event() 26 | { 27 | return Clutter.EVENT_STOP; 28 | } 29 | }); 30 | 31 | var AltPopupImage = GObject.registerClass( 32 | class AltPopupImage extends PopupMenu.PopupImageMenuItem 33 | { 34 | _init(text, icon) 35 | { 36 | super._init(text, icon); 37 | } 38 | 39 | /* Default temporary action for override */ 40 | _onItemClicked() 41 | { 42 | return null; 43 | } 44 | 45 | _onButtonReleaseEvent(actor, event) 46 | { 47 | return this.vfunc_button_release_event(); 48 | } 49 | 50 | vfunc_button_release_event() 51 | { 52 | this.remove_style_pseudo_class('active'); 53 | this._onItemClicked(); 54 | return Clutter.EVENT_STOP; 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /po/cast-to-tv/cast-to-tv.pot 3 | translation: /po/cast-to-tv/%two_letters_code%.po 4 | - source: /po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot 5 | translation: /po/cast-to-tv-links-addon/%two_letters_code%.po 6 | -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | Convenience replacement 3 | Original convenience.js does not work correctly with this extension use-cases 4 | */ 5 | 6 | const { Gio, GLib } = imports.gi; 7 | const ByteArray = imports.byteArray; 8 | const Gettext = imports.gettext; 9 | 10 | const NOTIFY_PATH = GLib.find_program_in_path('notify-send'); 11 | const GJS_PATH = GLib.find_program_in_path('gjs'); 12 | 13 | let launcher; 14 | let runApp; 15 | 16 | function getSettings(localPath, schemaName) 17 | { 18 | if(!localPath) return null; 19 | 20 | schemaName = schemaName || 'org.gnome.shell.extensions.cast-to-tv'; 21 | 22 | const GioSSS = Gio.SettingsSchemaSource; 23 | let schemaDir = Gio.File.new_for_path(localPath).get_child('schemas'); 24 | let schemaSource = null; 25 | 26 | if(schemaDir.query_exists(null)) 27 | schemaSource = GioSSS.new_from_directory( 28 | localPath + '/schemas', GioSSS.get_default(), false 29 | ); 30 | else 31 | schemaSource = GioSSS.get_default(); 32 | 33 | let schemaObj = schemaSource.lookup(schemaName, true); 34 | if(!schemaObj) 35 | throw new Error('Cast to TV: extension schemas could not be found!'); 36 | 37 | return new Gio.Settings({ settings_schema: schemaObj }); 38 | } 39 | 40 | function initTranslations(localPath, gettextDomain) 41 | { 42 | gettextDomain = gettextDomain || 'cast-to-tv'; 43 | 44 | if(localPath) 45 | { 46 | let localeDir = Gio.File.new_for_path(localPath).get_child('locale'); 47 | 48 | if(localeDir.query_exists(null)) 49 | Gettext.bindtextdomain(gettextDomain, localPath + '/locale'); 50 | else 51 | Gettext.bindtextdomain(gettextDomain, '/usr/share/locale'); 52 | } 53 | } 54 | 55 | function closeOtherApps(mainPath, totalKill) 56 | { 57 | if(runApp) 58 | { 59 | if(!runApp.get_identifier()) 60 | runApp = null; 61 | else 62 | { 63 | runApp.wait_async(null, () => runApp = null); 64 | runApp.force_exit(); 65 | } 66 | } 67 | 68 | /* Close other possible opened extension windows */ 69 | if(mainPath && totalKill) 70 | GLib.spawn_command_line_async('pkill -SIGINT -f ' + mainPath); 71 | } 72 | 73 | function startApp(appPath, appName, args, noClose) 74 | { 75 | appName = appName || 'app'; 76 | let spawnArgs = [GJS_PATH, appPath + '/' + appName + '.js']; 77 | 78 | if(args && Array.isArray(args)) 79 | args.forEach(arg => spawnArgs.push(arg)); 80 | 81 | /* To not freeze gnome shell app needs to be run as separate process */ 82 | if(noClose) return GLib.spawn_async(appPath, spawnArgs, null, 0, null); 83 | 84 | if(!launcher) 85 | launcher = new Gio.SubprocessLauncher(); 86 | 87 | launcher.set_cwd(appPath); 88 | 89 | if(!runApp || !runApp.get_identifier()) 90 | return runApp = launcher.spawnv(spawnArgs); 91 | 92 | runApp.wait_async(null, () => runApp = launcher.spawnv(spawnArgs)); 93 | runApp.force_exit(); 94 | } 95 | 96 | function setDevicesWidget(widget, devices, activeText) 97 | { 98 | if(Array.isArray(devices) && devices.length) 99 | { 100 | let foundActive = false; 101 | let appendIndex = 0; 102 | let appendArray = []; 103 | 104 | devices.forEach(device => 105 | { 106 | if( 107 | (!device.name 108 | || !device.friendlyName 109 | || appendArray.includes(device.friendlyName)) 110 | || (!device.name.endsWith('.local') 111 | && !device.ip) 112 | ) { 113 | return; 114 | } 115 | 116 | widget.append(device.friendlyName, device.friendlyName); 117 | appendArray.push(device.friendlyName); 118 | appendIndex++; 119 | 120 | if(!foundActive && activeText && activeText === device.friendlyName) 121 | { 122 | widget.set_active(appendIndex); 123 | foundActive = true; 124 | } 125 | }); 126 | 127 | if(activeText && !foundActive) 128 | widget.set_active(0); 129 | 130 | return; 131 | } 132 | 133 | widget.set_active(0); 134 | } 135 | 136 | function parsePlayercastDevices(localData, webData) 137 | { 138 | if(webData) 139 | { 140 | webData.forEach(fn => 141 | { 142 | if(localData.some(dev => dev.friendlyName === fn)) 143 | return; 144 | 145 | localData.unshift({ 146 | name: (fn.split(' ').join('')).toLowerCase() + '.local', 147 | friendlyName: fn, 148 | ip: '' 149 | }); 150 | }); 151 | } 152 | 153 | return localData; 154 | } 155 | 156 | function readFromFile(path) 157 | { 158 | let fileExists = GLib.file_test(path, GLib.FileTest.EXISTS); 159 | 160 | if(fileExists) 161 | { 162 | let [success, contents] = GLib.file_get_contents(path); 163 | 164 | if(success) 165 | { 166 | if(contents instanceof Uint8Array) 167 | { 168 | try { contents = JSON.parse(ByteArray.toString(contents)); } 169 | catch(err) { contents = null; } 170 | } 171 | else 172 | { 173 | try { contents = JSON.parse(contents); } 174 | catch(err) { contents = null; } 175 | } 176 | 177 | return contents; 178 | } 179 | } 180 | 181 | return null; 182 | } 183 | 184 | function readFromFileAsync(file, callback) 185 | { 186 | /* Either filepath or Gio.File can be used */ 187 | if(file && typeof file === 'string') 188 | file = Gio.file_new_for_path(file); 189 | 190 | file.load_contents_async(null, (file, res) => 191 | { 192 | let success, contents; 193 | 194 | try { 195 | [success, contents] = file.load_contents_finish(res); 196 | 197 | if(success) 198 | { 199 | if(contents instanceof Uint8Array) 200 | contents = JSON.parse(ByteArray.toString(contents)); 201 | else 202 | contents = JSON.parse(contents); 203 | } 204 | else 205 | contents = null; 206 | } 207 | catch(err) { 208 | contents = null; 209 | } 210 | 211 | callback(contents); 212 | }); 213 | } 214 | 215 | function writeToFile(path, contents) 216 | { 217 | GLib.file_set_contents(path, JSON.stringify(contents, null, 1)); 218 | } 219 | 220 | function readOutputAsync(stream, callback) 221 | { 222 | stream.read_line_async(GLib.PRIORITY_LOW, null, (source, res) => 223 | { 224 | let out_fd, length, outStr; 225 | 226 | [out_fd, length] = source.read_line_finish(res); 227 | 228 | if(out_fd !== null) 229 | { 230 | if(out_fd instanceof Uint8Array) 231 | outStr = ByteArray.toString(out_fd); 232 | else 233 | outStr = out_fd.toString(); 234 | 235 | callback(outStr, false); 236 | return readOutputAsync(source, callback); 237 | } 238 | 239 | callback('', true); 240 | }); 241 | } 242 | 243 | function createDir(dirPath, permissions) 244 | { 245 | permissions = permissions || 493 // 755 in octal 246 | 247 | let dirExists = GLib.file_test(dirPath, GLib.FileTest.EXISTS); 248 | 249 | if(!dirExists) 250 | GLib.mkdir_with_parents(dirPath, permissions); 251 | } 252 | 253 | function notify(summary, mainBody) 254 | { 255 | if(NOTIFY_PATH) 256 | { 257 | GLib.spawn_async(null, [ 258 | NOTIFY_PATH, 259 | '-i', 'tv-symbolic', 260 | '-u', 'normal', 261 | summary, mainBody 262 | ], null, 0, null); 263 | } 264 | 265 | log(summary + ': ' + mainBody); 266 | } 267 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Cast files to Chromecast, web browser or media player app over local network.\n\nRequires: npm nodejs ffmpeg\n\nAfter enabling go to Cast Settings \u2192 Modules and click \"Install npm modules\" button.\n\nVisit this extension homepage for more info on how to install and use.\nPlease use GitHub to report issues.", 3 | "extension-id": "cast-to-tv", 4 | "gettext-domain": "cast-to-tv", 5 | "name": "Cast to TV", 6 | "settings-schema": "org.gnome.shell.extensions.cast-to-tv", 7 | "shell-version": [ 8 | "3.34", 9 | "3.36" 10 | ], 11 | "url": "https://rafostar.github.io/gnome-shell-extension-cast-to-tv", 12 | "uuid": "cast-to-tv@rafostar.github.com", 13 | "version": 15 14 | } 15 | -------------------------------------------------------------------------------- /node_scripts/addons-importer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const debug = require('debug')('addons-importer'); 4 | const extensionsPath = path.join(__dirname + '/../..'); 5 | 6 | var addons = []; 7 | 8 | module.exports = function(name) 9 | { 10 | return addons[name]; 11 | } 12 | 13 | fs.readdir(extensionsPath, (err, extensions) => 14 | { 15 | extensions.forEach(folder => 16 | { 17 | if(!folder.startsWith('cast-to-tv') || !folder.includes('addon@')) 18 | return; 19 | 20 | debug(`Addon folder: ${folder}`); 21 | 22 | var addonPath = path.join(extensionsPath, folder, 'node_scripts/addon'); 23 | debug(`Addon path: ${addonPath}`); 24 | 25 | var addonName = folder.substring(11, folder.lastIndexOf('-')); 26 | debug(`Addon name: ${addonName}`); 27 | 28 | fs.access(addonPath + '.js', fs.constants.F_OK, (err) => 29 | { 30 | if(err) return debug(err); 31 | 32 | addons[addonName] = require(addonPath); 33 | debug(`Imported: ${addonName}`); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /node_scripts/encode.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const debug = require('debug')('ffmpeg'); 3 | const bridge = require('./bridge'); 4 | const notify = require('./notify'); 5 | const messages = require('./messages.js'); 6 | const shared = require('../shared'); 7 | const stdioConf = (debug.enabled) ? 'inherit' : 'ignore'; 8 | 9 | exports.streamProcess = null; 10 | exports.enabled = true; 11 | var notifyError = false; 12 | 13 | String.prototype.replaceAt = function(index, replacement) 14 | { 15 | return this.substr(0, index) + replacement + this.substr(index + 1); 16 | } 17 | 18 | function getSubsOptions() 19 | { 20 | var subsPathEscaped = (bridge.selection.subsPath) ? bridge.selection.subsPath : bridge.selection.filePath; 21 | var index = subsPathEscaped.length; 22 | 23 | debug('Parsing subtitles path...'); 24 | 25 | while(index--) 26 | { 27 | if(shared.escapeChars.includes(subsPathEscaped.charAt(index))) 28 | subsPathEscaped = subsPathEscaped.replaceAt(index, '\\' + subsPathEscaped.charAt(index)); 29 | } 30 | 31 | debug(`Parsed subtitles path: ${subsPathEscaped}`); 32 | 33 | if(bridge.mediaData.charEnc) 34 | { 35 | subsPathEscaped += ':charenc=' + bridge.mediaData.charEnc; 36 | debug(`Added ${bridge.mediaData.charEnc} char encoding to subtitles options`); 37 | } 38 | 39 | return subsPathEscaped; 40 | } 41 | 42 | function getAudioOptsArray(isEncodeAudio) 43 | { 44 | return (isEncodeAudio && isEncodeAudio === true) ? ['flac', '-ac', '2'] : ['copy']; 45 | } 46 | 47 | function getPlayerOptsArray() 48 | { 49 | if(bridge.config.receiverType === 'playercast') 50 | return ['-f', 'matroska']; 51 | 52 | return [ 53 | '-frag_duration', '1000000', 54 | '-movflags', '+empty_moov', 55 | '-strict', '-2', 56 | '-f', 'mp4' 57 | ]; 58 | } 59 | 60 | function getIsBurnSubs() 61 | { 62 | return ( 63 | bridge.config.burnSubtitles 64 | && (bridge.mediaData.isSubsMerged || bridge.selection.subsPath) 65 | ) ? true : false; 66 | } 67 | 68 | function insertSubsAfter(encodeOpts, arrEl) 69 | { 70 | if(!getIsBurnSubs()) 71 | return; 72 | 73 | encodeOpts.splice( 74 | encodeOpts.indexOf(arrEl) + 1, 0, 75 | '-vf', 'subtitles=' + getSubsOptions(), '-sn' 76 | ); 77 | } 78 | 79 | function createEncodeProcess(encodeOpts) 80 | { 81 | debug(`Starting FFmpeg with opts: ${JSON.stringify(encodeOpts)}`); 82 | 83 | exports.streamProcess = spawn(bridge.config.ffmpegPath, encodeOpts, 84 | { stdio: ['ignore', 'pipe', stdioConf] }); 85 | 86 | notifyError = false; 87 | exports.streamProcess.once('exit', onAutoExit); 88 | exports.streamProcess.once('error', onEncodeError); 89 | 90 | return exports.streamProcess.stdout; 91 | } 92 | 93 | function onAutoExit(code) 94 | { 95 | if(!notifyError) 96 | { 97 | exports.streamProcess.removeListener('error', onEncodeError); 98 | 99 | if(code) notify('Cast to TV', messages.ffmpegError, bridge.selection.filePath); 100 | } 101 | 102 | exports.streamProcess = null; 103 | 104 | if(code !== null) 105 | debug(`FFmpeg exited with code: ${code}`); 106 | 107 | debug('FFmpeg auto exit'); 108 | } 109 | 110 | function onManualExit(code) 111 | { 112 | if(!notifyError) 113 | exports.streamProcess.removeListener('error', onEncodeError); 114 | 115 | exports.streamProcess = null; 116 | debug('FFmpeg manual exit'); 117 | } 118 | 119 | function onEncodeError(error) 120 | { 121 | if(error.message == 'spawn ' + bridge.config.ffmpegPath + ' ENOENT') 122 | { 123 | notify('Cast to TV', messages.ffmpegPath); 124 | notifyError = true; 125 | } 126 | 127 | exports.streamProcess = null; 128 | debug('FFmpeg had error!'); 129 | debug(error); 130 | } 131 | 132 | exports.video = function(isEncodeAudio) 133 | { 134 | var encodeOpts = [ 135 | '-i', bridge.selection.filePath, 136 | '-c:v', 'libx264', 137 | '-pix_fmt', 'yuv420p', 138 | '-preset', 'superfast', 139 | '-level:v', '4.1', 140 | '-b:v', bridge.config.videoBitrate + 'M', 141 | '-c:a', ...getAudioOptsArray(isEncodeAudio), 142 | '-metadata', 'title=Cast to TV - Software Encoded Stream', 143 | ...getPlayerOptsArray(), 144 | 'pipe:1' 145 | ]; 146 | 147 | insertSubsAfter(encodeOpts, 'libx264'); 148 | 149 | return createEncodeProcess(encodeOpts); 150 | } 151 | 152 | exports.videoVaapi = function(isEncodeAudio) 153 | { 154 | var encodeOpts = [ 155 | '-i', bridge.selection.filePath, 156 | '-c:v', 'h264_vaapi', 157 | '-level:v', '4.1', 158 | '-b:v', bridge.config.videoBitrate + 'M', 159 | '-c:a', ...getAudioOptsArray(isEncodeAudio), 160 | '-metadata', 'title=Cast to TV - VAAPI Encoded Stream', 161 | ...getPlayerOptsArray(), 162 | 'pipe:1' 163 | ]; 164 | 165 | if(getIsBurnSubs()) 166 | { 167 | encodeOpts.unshift( 168 | '-hwaccel', 'vaapi', 169 | '-hwaccel_device', '/dev/dri/renderD128', 170 | '-hwaccel_output_format', 'vaapi' 171 | ); 172 | encodeOpts.splice( 173 | encodeOpts.indexOf('h264_vaapi') + 1, 0, 174 | '-vf', 'scale_vaapi,hwmap=mode=read+write,format=nv12,subtitles=' + 175 | getSubsOptions() + ',hwmap', '-sn' 176 | ); 177 | } 178 | else 179 | { 180 | encodeOpts.unshift('-vaapi_device', '/dev/dri/renderD128'); 181 | encodeOpts.splice(encodeOpts.indexOf('h264_vaapi') + 1, 0, '-vf', 'format=nv12,hwmap'); 182 | } 183 | 184 | return createEncodeProcess(encodeOpts); 185 | } 186 | 187 | exports.videoNvenc = function(isEncodeAudio) 188 | { 189 | var encodeOpts = [ 190 | '-i', bridge.selection.filePath, 191 | '-c:v', 'h264_nvenc', 192 | '-level:v', '4.1', 193 | '-b:v', bridge.config.videoBitrate + 'M', 194 | '-c:a', ...getAudioOptsArray(isEncodeAudio), 195 | '-metadata', 'title=Cast to TV - NVENC Encoded Stream', 196 | ...getPlayerOptsArray(), 197 | 'pipe:1' 198 | ]; 199 | 200 | insertSubsAfter(encodeOpts, 'h264_nvenc'); 201 | 202 | return createEncodeProcess(encodeOpts); 203 | } 204 | 205 | exports.videoAmf = function(isEncodeAudio) 206 | { 207 | var encodeOpts = [ 208 | '-i', bridge.selection.filePath, 209 | '-c:v', 'h264_amf', 210 | '-level:v', '4.1', 211 | '-b:v', bridge.config.videoBitrate + 'M', 212 | '-c:a', ...getAudioOptsArray(isEncodeAudio), 213 | '-metadata', 'title=Cast to TV - AMF Encoded Stream', 214 | ...getPlayerOptsArray(), 215 | 'pipe:1' 216 | ]; 217 | 218 | insertSubsAfter(encodeOpts, 'h264_amf'); 219 | 220 | return createEncodeProcess(encodeOpts); 221 | } 222 | 223 | exports.audio = function() 224 | { 225 | var encodeOpts = [ 226 | '-i', bridge.selection.filePath, 227 | '-c:v', 'copy', 228 | '-c:a', ...getAudioOptsArray(true), 229 | '-metadata', 'title=Cast to TV - Audio Encoded Stream', 230 | ...getPlayerOptsArray(), 231 | 'pipe:1' 232 | ]; 233 | 234 | return createEncodeProcess(encodeOpts); 235 | } 236 | 237 | exports.musicVisualizer = function() 238 | { 239 | var encodeOpts = [ 240 | '-i', bridge.selection.filePath, 241 | '-filter_complex', 242 | `firequalizer=gain='(1.4884e8 * f*f*f / (f*f + 424.36) / (f*f + 1.4884e8) / sqrt(f*f + 25122.25)) / sqrt(2)': 243 | scale=linlin: 244 | wfunc=tukey: 245 | zero_phase=on: 246 | fft2=on, 247 | showcqt=fps=60: 248 | size=1280x360: 249 | count=1: 250 | csp=bt470bg: 251 | cscheme=1|0|0.5|0|1|0.5: 252 | bar_g=2: 253 | sono_g=4: 254 | bar_v=9: 255 | sono_v=17: 256 | sono_h=0: 257 | bar_t=0.5: 258 | axis_h=0: 259 | tc=0.33: 260 | tlength='st(0,0.17); 384*tc / (384 / ld(0) + tc*f /(1-ld(0))) + 384*tc / (tc*f / ld(0) + 384 /(1-ld(0)))', 261 | format=yuv420p,split [v0],vflip [v1]; [v0][v1] vstack [vis]`, 262 | '-map', '[vis]', 263 | '-map', '0:a', 264 | '-c:v', 'libx264', 265 | '-pix_fmt', 'yuv420p', 266 | '-preset', 'superfast', 267 | '-level:v', '4.1', 268 | '-b:v', bridge.config.videoBitrate + 'M', 269 | '-c:a', 'copy', 270 | '-metadata', 'title=Cast to TV - Music Visualizer', 271 | ...getPlayerOptsArray(), 272 | 'pipe:1' 273 | ]; 274 | 275 | return createEncodeProcess(encodeOpts); 276 | } 277 | 278 | exports.closeStreamProcess = function() 279 | { 280 | if(!exports.streamProcess) return; 281 | 282 | if(exports.streamProcess.stdout) 283 | { 284 | exports.streamProcess.removeListener('exit', onAutoExit); 285 | exports.streamProcess.once('exit', onManualExit); 286 | 287 | if(!exports.streamProcess.stdout.destroyed) 288 | { 289 | exports.streamProcess.stdout.destroy(); 290 | debug('Destroyed stream process remaining stdout data'); 291 | } 292 | else 293 | debug('Stream process stdout was destroyed earlier'); 294 | 295 | exports.streamProcess.stdout = null; 296 | } 297 | else 298 | { 299 | debug('Force killing stream process...'); 300 | 301 | try { 302 | process.kill(exports.streamProcess.pid, 'SIGHUP'); 303 | debug('Force killed stream process'); 304 | } 305 | catch(err) { 306 | debug('Could not kill stream process!'); 307 | debug(err); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /node_scripts/events.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | var ee = new EventEmitter(); 3 | 4 | module.exports = ee; 5 | -------------------------------------------------------------------------------- /node_scripts/gettext.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Gettext = require('node-gettext'); 4 | const moParser = require('gettext-parser').mo; 5 | 6 | const extLocaleDir = path.join(__dirname + '/../locale'); 7 | const translationsDir = (fs.existsSync(extLocaleDir)) ? extLocaleDir : '/usr/share/locale'; 8 | const domain = 'cast-to-tv'; 9 | 10 | var gt = new Gettext(); 11 | exports.locales = []; 12 | 13 | exports.initTranslations = function() 14 | { 15 | gt.setTextDomain(domain); 16 | 17 | exports.locales = fs.readdirSync(translationsDir); 18 | 19 | exports.locales.forEach(locale => 20 | { 21 | var translationsFilePath = path.join(translationsDir, locale, 'LC_MESSAGES', domain + '.mo'); 22 | fs.access(translationsFilePath, fs.constants.F_OK, (err) => 23 | { 24 | if(err) return; 25 | 26 | fs.readFile(translationsFilePath, (err, data) => 27 | { 28 | if(err) return console.log(`Cast to TV: error reading node ${locale} translation`); 29 | 30 | var parsedTranslations = moParser.parse(data); 31 | gt.addTranslations(locale, domain, parsedTranslations); 32 | }); 33 | }); 34 | }); 35 | 36 | exports.locales.unshift('en'); 37 | } 38 | 39 | exports.setLocale = function(locale) 40 | { 41 | gt.setLocale(locale); 42 | } 43 | 44 | exports.translate = function(text) 45 | { 46 | return gt.gettext(text); 47 | } 48 | -------------------------------------------------------------------------------- /node_scripts/gnome.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { spawn, spawnSync } = require('child_process'); 4 | const debug = require('debug')('gnome'); 5 | const sender = require('./sender'); 6 | const noop = () => {}; 7 | 8 | var schemaName = 'org.gnome.shell.extensions.cast-to-tv'; 9 | var schemaDir = path.join(__dirname + '/../schemas'); 10 | var isSchema = false; 11 | 12 | module.exports = 13 | { 14 | isRemote: false, 15 | isLockScreen: false, 16 | 17 | loadSchema: function(customName, customPath) 18 | { 19 | schemaName = customName || schemaName; 20 | schemaDir = customPath || schemaDir; 21 | 22 | isSchema = fs.existsSync(`${schemaDir}/gschemas.compiled`); 23 | debug(`Settings schema available: ${isSchema}`); 24 | }, 25 | 26 | setSetting: function(setting, value, cb) 27 | { 28 | cb = cb || noop; 29 | 30 | var args = ['set', schemaName, setting, value]; 31 | if(isSchema) args.unshift('--schemadir', schemaDir); 32 | 33 | debug(`Set ${setting}: ${value}`); 34 | var gProcess = spawn('gsettings', args); 35 | gProcess.once('exit', cb); 36 | }, 37 | 38 | getSetting: function(setting) 39 | { 40 | var args = ['get', schemaName, setting]; 41 | if(isSchema) args.unshift('--schemadir', schemaDir); 42 | 43 | var gsettings = spawnSync('gsettings', args); 44 | var value = String(gsettings.stdout).replace(/\n/, '').replace(/\'/g, ''); 45 | debug(`Get ${setting}: ${value}`); 46 | 47 | return value; 48 | }, 49 | 50 | getBoolean: function(setting) 51 | { 52 | var value = this.getSetting(setting); 53 | return (value === 'true' || value === true); 54 | }, 55 | 56 | getJSON: function(setting) 57 | { 58 | var value = this.getSetting(setting); 59 | return JSON.parse(value); 60 | }, 61 | 62 | showRemote: function(enable, playbackData, cb) 63 | { 64 | cb = cb || noop; 65 | this.isRemote = enable; 66 | 67 | if(this.isLockScreen) 68 | return cb(null); 69 | 70 | debug(`Show remote widget: ${enable}`); 71 | 72 | var data = { isPlaying: enable }; 73 | 74 | if(playbackData) 75 | data = { ...playbackData, ...data }; 76 | 77 | sender.sendPlaybackData(data, cb); 78 | }, 79 | 80 | getTempConfig: function() 81 | { 82 | var config = { 83 | ffmpegPath: this.getSetting('ffmpeg-path'), 84 | ffprobePath: this.getSetting('ffprobe-path'), 85 | receiverType: this.getSetting('receiver-type'), 86 | listeningPort: this.getSetting('listening-port'), 87 | internalPort: this.getSetting('internal-port'), 88 | webplayerSubs: parseFloat(this.getSetting('webplayer-subs')).toFixed(1), 89 | videoBitrate: parseFloat(this.getSetting('video-bitrate')).toFixed(1), 90 | videoAcceleration: this.getSetting('video-acceleration'), 91 | burnSubtitles: this.getBoolean('burn-subtitles'), 92 | musicVisualizer: this.getBoolean('music-visualizer'), 93 | slideshowTime: this.getSetting('slideshow-time'), 94 | extractorReuse: this.getBoolean('extractor-reuse'), 95 | subsPreferred: this.getSetting('subs-preferred'), 96 | subsFallback: this.getSetting('subs-fallback'), 97 | extractorDir: this.getSetting('extractor-dir'), 98 | chromecastName: this.getSetting('chromecast-name'), 99 | playercastName: this.getSetting('playercast-name'), 100 | chromecastDevices: this.getJSON('chromecast-devices'), 101 | chromecastSubtitles: this.getJSON('chromecast-subtitles') 102 | }; 103 | 104 | /* Use default paths if custom paths are not defined */ 105 | if(!config.ffmpegPath) config.ffmpegPath = '/usr/bin/ffmpeg'; 106 | if(!config.ffprobePath) config.ffprobePath = '/usr/bin/ffprobe'; 107 | 108 | if(!config.subsPreferred) 109 | config.subsPreferred = 'eng/English'; 110 | 111 | return config; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /node_scripts/messages.js: -------------------------------------------------------------------------------- 1 | function _(text) { return text; } 2 | 3 | module.exports = { 4 | loading: _("LOADING"), 5 | noMedia: _("No file selected"), 6 | receiverChromecast: _("Receiver type is set to Chromecast"), 7 | receiverPlayercast: _("Receiver type is set to Playercast app"), 8 | streamActive: _("Streaming process is still active"), 9 | /* TRANSLATORS: Do not remove HTML tags */ 10 | connectLimit: _("Too many connections!

Close all other tabs that are accessing this page in all browsers

"), 11 | /* TRANSLATORS: This sentence will contain file path after end */ 12 | ffmpegError: _("FFmpeg could not transcode file:"), 13 | ffmpegPath: _("FFmpeg path is incorrect"), 14 | /* TRANSLATORS: This sentence will contain file path after end */ 15 | ffprobeError: _("FFprobe could not process file:"), 16 | ffprobePath: _("FFprobe path is incorrect"), 17 | /* TRANSLATORS: This sentence will contain file path after end */ 18 | extractError: _("Could not analyze selected file:"), 19 | plyr: { 20 | speed: _("Speed"), 21 | /* TRANSLATORS: One of "Speed" setting value */ 22 | normal: _("Normal") 23 | }, 24 | chromecast: { 25 | notFound: _("Device not found"), 26 | loadFailed: _("Failed to load media"), 27 | connectFailed: _("Could not connect to device"), 28 | verifyIp: _("Verify device IP in extension preferences"), 29 | /* TRANSLATORS: This sentence will contain file path after end */ 30 | playError: _("Could not play file:"), 31 | tryAgain: _("Try again with transcoding enabled") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /node_scripts/notify.js: -------------------------------------------------------------------------------- 1 | const gettext = require('./gettext'); 2 | const { spawn } = require('child_process'); 3 | 4 | const sysLang = process.env.LANG.substring(0, 2); 5 | 6 | module.exports = function(summary, mainBody, data, infoBody) 7 | { 8 | if(!summary || !mainBody) return; 9 | 10 | gettext.setLocale(sysLang); 11 | mainBody = gettext.translate(mainBody); 12 | 13 | if(data && typeof data === 'string') 14 | mainBody += ` ${data}`; 15 | 16 | if(infoBody && typeof infoBody === 'string') 17 | mainBody += '.\n' + gettext.translate(infoBody) + '.'; 18 | 19 | spawn('notify-send', [ 20 | '-i', 'tv-symbolic', 21 | '-u', 'normal', 22 | summary, mainBody 23 | ]); 24 | 25 | console.log(summary + ': ' + mainBody); 26 | } 27 | -------------------------------------------------------------------------------- /node_scripts/playercast.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('playercast'); 2 | const scanner = require('multicast-scanner'); 3 | const internalIp = require('internal-ip').v4; 4 | const bridge = require('./bridge'); 5 | const socket = require('./server-socket'); 6 | const sender = require('./sender'); 7 | const events = require('./events'); 8 | 9 | var connectTimeout; 10 | 11 | exports.cast = function() 12 | { 13 | var recName = null; 14 | var foundDev = null; 15 | 16 | if(bridge.config.playercastName) 17 | { 18 | var isConnected = socket.playercasts.some(dev => dev === bridge.config.playercastName); 19 | 20 | if(isConnected) 21 | { 22 | recName = bridge.config.playercastName; 23 | debug(`Cast to already connected playercast: ${recName}`); 24 | } 25 | } 26 | 27 | if(!bridge.config.playercastName && socket.playercasts.length) 28 | { 29 | recName = socket.playercasts[0]; 30 | debug(`Cast to first playercast: ${recName}`); 31 | } 32 | 33 | if(recName) 34 | return emitCast(recName); 35 | 36 | recName = bridge.config.playercastName || null; 37 | debug(`Searching for ${recName || 'any Playercast'}...`); 38 | 39 | findReceiver(recName, (err, device) => 40 | { 41 | if(err) return debug(err); 42 | 43 | debug('Playercast found'); 44 | connectClient(device); 45 | }); 46 | } 47 | 48 | function emitCast(receiverName) 49 | { 50 | socket.emit('playercast', { 51 | name: receiverName, 52 | mediaData: bridge.mediaData, 53 | ...bridge.selection 54 | }); 55 | } 56 | 57 | function findReceiver(receiverName, cb) 58 | { 59 | const opts = { 60 | friendly_name: receiverName, 61 | service_name: '_playercast._tcp.local' 62 | }; 63 | 64 | scanner(opts, cb); 65 | } 66 | 67 | function connectClient(device) 68 | { 69 | const reqOpts = { 70 | hostname: device.ip, 71 | port: device.port || 9881 72 | }; 73 | 74 | debug('Checking sender IP...'); 75 | 76 | internalIp().then(address => 77 | { 78 | if(!address) 79 | return debug(new Error('Local IP undetected')); 80 | 81 | debug(`Sender IP: ${address}`); 82 | 83 | const reqData = { 84 | hostname: address, 85 | port: bridge.config.listeningPort 86 | }; 87 | 88 | debug('Sending connection request...'); 89 | sender.sendPlayercastRequest(reqOpts, reqData, (err) => 90 | { 91 | if(err) return debug(err); 92 | 93 | debug('Send connection request'); 94 | 95 | events.on('playercast-added', onPlayercastAdded); 96 | setConnectTimeout(); 97 | }); 98 | }); 99 | } 100 | 101 | function setConnectTimeout() 102 | { 103 | if(connectTimeout) return; 104 | 105 | connectTimeout = setTimeout(() => 106 | { 107 | connectTimeout = null; 108 | 109 | debug('Playercast connection timeout'); 110 | events.removeListener('playercast-added', onPlayercastAdded); 111 | }, 7000); 112 | 113 | debug('Started connection timeout'); 114 | } 115 | 116 | function clearConnectTimeout() 117 | { 118 | if(!connectTimeout) return; 119 | 120 | clearTimeout(connectTimeout); 121 | connectTimeout = null; 122 | 123 | debug('Removed connection timeout'); 124 | } 125 | 126 | function onPlayercastAdded(addedName) 127 | { 128 | debug('New Playercast added'); 129 | 130 | if(bridge.config.playercastName) 131 | { 132 | if(addedName !== bridge.config.playercastName) 133 | return debug('Playercast name mismatch'); 134 | } 135 | 136 | clearConnectTimeout(); 137 | events.removeListener('playercast-added', onPlayercastAdded); 138 | 139 | emitCast(addedName); 140 | } 141 | -------------------------------------------------------------------------------- /node_scripts/remote-controller.js: -------------------------------------------------------------------------------- 1 | const bridge = require('./bridge'); 2 | const socket = require('./server-socket'); 3 | const gnome = require('./gnome'); 4 | 5 | exports.repeat = false; 6 | exports.slideshow = false; 7 | 8 | var slideshowTimeout; 9 | 10 | exports.webControl = function(action, value) 11 | { 12 | var currentTrackID; 13 | var listLastID; 14 | 15 | switch(action) 16 | { 17 | case 'SKIP+': 18 | currentTrackID = bridge.playlist.indexOf(bridge.selection.filePath) + 1; 19 | listLastID = bridge.playlist.length; 20 | if(currentTrackID < listLastID) exports.changeTrack(currentTrackID + 1); 21 | break; 22 | case 'SKIP-': 23 | currentTrackID = bridge.playlist.indexOf(bridge.selection.filePath) + 1; 24 | if(currentTrackID > 1) exports.changeTrack(currentTrackID - 1); 25 | break; 26 | case 'REPEAT': 27 | exports.repeat = value; 28 | break; 29 | case 'SLIDESHOW': 30 | exports.slideshow = value; 31 | if(exports.slideshow) 32 | exports.setSlideshow(); 33 | else 34 | exports.clearSlideshow(); 35 | break; 36 | default: 37 | if(typeof value !== 'undefined') 38 | socket.emit('remote-signal', { action, value }); 39 | else 40 | socket.emit('remote-signal', { action }); 41 | break; 42 | } 43 | } 44 | 45 | exports.changeTrack = function(id) 46 | { 47 | /* Tracks are counted from 1, list indexes from 0 */ 48 | bridge.selection.filePath = bridge.playlist[id - 1]; 49 | bridge.selection.subsPath = ""; 50 | 51 | bridge.updateSelection(bridge.selection); 52 | } 53 | 54 | exports.checkNextTrack = function() 55 | { 56 | var currentTrackID = bridge.playlist.indexOf(bridge.selection.filePath) + 1; 57 | var listLastID = bridge.playlist.length; 58 | 59 | if(exports.repeat && currentTrackID === listLastID) 60 | { 61 | exports.changeTrack(1); 62 | return true; 63 | } 64 | else if(currentTrackID < listLastID) 65 | { 66 | exports.changeTrack(currentTrackID + 1); 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | exports.clearSlideshow = function() 74 | { 75 | if(!slideshowTimeout) 76 | return false; 77 | 78 | clearTimeout(slideshowTimeout); 79 | slideshowTimeout = null; 80 | 81 | return true; 82 | } 83 | 84 | exports.setSlideshow = function() 85 | { 86 | exports.clearSlideshow(); 87 | 88 | if(exports.slideshow && bridge.selection.streamType === 'PICTURE') 89 | { 90 | var time = bridge.config.slideshowTime * 1000; 91 | 92 | slideshowTimeout = setTimeout(() => 93 | { 94 | slideshowTimeout = null; 95 | var trackChanged = exports.checkNextTrack(); 96 | 97 | if(!trackChanged) 98 | bridge.handleRemoteSignal('STOP'); 99 | }, time); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /node_scripts/remove.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const debug = require('debug')('remove'); 3 | const shared = require('../shared'); 4 | const noop = () => {}; 5 | 6 | exports.file = function(filePath, cb) 7 | { 8 | cb = cb || noop; 9 | 10 | debug(`Removing ${filePath}...`); 11 | fs.access(filePath, fs.constants.F_OK, (err) => 12 | { 13 | if(err) 14 | { 15 | var noFileMsg = `File ${filePath} does not exist`; 16 | debug(noFileMsg); 17 | 18 | return cb(new Error(noFileMsg)); 19 | } 20 | 21 | fs.unlink(filePath, (err) => 22 | { 23 | if(!err) 24 | { 25 | debug(`Removed ${filePath}`); 26 | return cb(null); 27 | } 28 | 29 | var noUnlinkMsg = `Could not remove file: ${filePath}`; 30 | debug(noUnlinkMsg); 31 | 32 | return cb(new Error(noUnlinkMsg)); 33 | }); 34 | }); 35 | } 36 | 37 | exports.tempCover = function(cb) 38 | { 39 | cb = cb || noop; 40 | 41 | exports.file(shared.coverDefault + '.jpg', cb); 42 | } 43 | 44 | exports.tempSubs = function(cb) 45 | { 46 | cb = cb || noop; 47 | 48 | exports.file(shared.vttSubsPath, cb); 49 | } 50 | -------------------------------------------------------------------------------- /node_scripts/sender.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const debug = require('debug')('sender'); 3 | const noop = () => {}; 4 | 5 | var lastData; 6 | 7 | module.exports = 8 | { 9 | enabled: false, 10 | 11 | configure: function(port) 12 | { 13 | this.opts = { 14 | host: '127.0.0.1', 15 | port: port, 16 | path: '/', 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json' 20 | }, 21 | timeout: 3000 22 | }; 23 | 24 | debug(`Sender configured to port: ${this.opts.port}`); 25 | }, 26 | 27 | _httpRequest: function(opts, data, cb) 28 | { 29 | var req = http.request(opts, () => 30 | { 31 | debug('Received response'); 32 | req.removeListener('error', debug); 33 | }); 34 | req.on('error', debug); 35 | req.end(data, cb); 36 | }, 37 | 38 | send: function(type, data, cb) 39 | { 40 | cb = cb || noop; 41 | 42 | if(!this.enabled) 43 | return cb(null); 44 | 45 | if(!this.opts) 46 | return cb(new Error('Sender not configured')); 47 | 48 | var dataString = JSON.stringify(data); 49 | 50 | /* Do not send same data */ 51 | if(dataString === lastData) 52 | return cb(null); 53 | 54 | lastData = dataString; 55 | this.opts.path = '/api/' + type; 56 | 57 | this._httpRequest(this.opts, dataString, cb); 58 | debug(`Send data: ${dataString}`); 59 | }, 60 | 61 | sendPlaybackStatus: function(status, cb) 62 | { 63 | this.send('status', status, cb); 64 | }, 65 | 66 | sendPlaybackData: function(data, cb) 67 | { 68 | this.send('data', data, cb); 69 | }, 70 | 71 | sendBrowserName: function(name, cb) 72 | { 73 | this.send('browser', { name: name }, cb); 74 | }, 75 | 76 | sendPlayercastRequest: function(opts, data, cb) 77 | { 78 | const reqOpts = { 79 | host: opts.hostname || '127.0.0.1', 80 | port: opts.port || 9881, 81 | path: opts.path || '/api/connect', 82 | method: opts.method || 'POST', 83 | timeout: 3000, 84 | headers: { 85 | 'Content-Type': 'application/json' 86 | } 87 | }; 88 | 89 | var dataString = JSON.stringify(data); 90 | this._httpRequest(reqOpts, dataString, cb); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /node_scripts/server-socket.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const io = require('socket.io'); 3 | const WebSocket = require('ws'); 4 | const extract = require('ffmpeg-extract'); 5 | const debug = require('debug')('socket'); 6 | const bridge = require('./bridge'); 7 | const encode = require('./encode'); 8 | const gettext = require('./gettext'); 9 | const messages = require('./messages'); 10 | const gnome = require('./gnome'); 11 | const controller = require('./remote-controller'); 12 | const sender = require('./sender'); 13 | const events = require('./events'); 14 | const shared = require('../shared'); 15 | 16 | var clientTimeout; 17 | var reconnectTimeout; 18 | var websocket; 19 | var wsConnected; 20 | 21 | exports.activeConnections = 0; 22 | exports.playercasts = []; 23 | 24 | exports.listen = function(server) 25 | { 26 | websocket = io.listen(server); 27 | websocket.on('connection', handleMessages); 28 | } 29 | 30 | exports.emit = function(message, opts) 31 | { 32 | websocket.emit(message, opts); 33 | } 34 | 35 | exports.connectWs = function(port) 36 | { 37 | if(gnome.isLockScreen || wsConnected) 38 | return; 39 | 40 | if(reconnectTimeout) 41 | { 42 | clearTimeout(reconnectTimeout); 43 | reconnectTimeout = null; 44 | } 45 | 46 | port = port || bridge.config.internalPort; 47 | 48 | debug(`Connecting to GNOME websocket on port: ${port}`); 49 | var ws = new WebSocket(`ws://127.0.0.1:${port}/websocket/node`); 50 | 51 | const onConnOpen = function() 52 | { 53 | debug('GNOME websocket connected'); 54 | ws.send('connected'); 55 | wsConnected = true; 56 | sender.enabled = true; 57 | } 58 | 59 | const onConnClose = function() 60 | { 61 | debug('GNOME websocket disconnected'); 62 | ws.removeAllListeners(); 63 | wsConnected = false; 64 | sender.enabled = false; 65 | 66 | if(!gnome.isLockScreen) 67 | { 68 | if(reconnectTimeout) 69 | clearTimeout(reconnectTimeout); 70 | 71 | reconnectTimeout = setTimeout(() => 72 | { 73 | reconnectTimeout = null; 74 | exports.connectWs(bridge.config.internalPort); 75 | }, 4250); 76 | } 77 | } 78 | 79 | ws.once('open', onConnOpen); 80 | ws.once('close', onConnClose); 81 | ws.once('error', onConnClose); 82 | } 83 | 84 | function handleMessages(socket) 85 | { 86 | exports.activeConnections++; 87 | 88 | if(clientTimeout) 89 | { 90 | clearTimeout(clientTimeout); 91 | clientTimeout = null; 92 | } 93 | 94 | socket.on('webplayer', msg => 95 | { 96 | switch(msg) 97 | { 98 | case 'webplayer-ask': 99 | initWebPlayer(); 100 | controller.setSlideshow(); 101 | break; 102 | case 'track-ended': 103 | controller.checkNextTrack(); 104 | break; 105 | case 'processes-ask': 106 | if(!extract.video.subsProcess && !extract.music.coverProcess) 107 | websocket.emit('processes-done', true); 108 | else 109 | websocket.emit('processes-done', false); 110 | break; 111 | case 'loading-ask': 112 | websocket.emit('loading-text', gettext.translate(messages.loading)); 113 | break; 114 | case 'message-ask': 115 | sendMessage(); 116 | break; 117 | default: 118 | break; 119 | } 120 | }); 121 | 122 | socket.on('playercast-connect', msg => 123 | { 124 | socket.playercastName = msg; 125 | 126 | if(exports.activeConnections > 0) 127 | exports.activeConnections--; 128 | 129 | if(!exports.playercasts.includes(socket.playercastName)) 130 | { 131 | exports.playercasts.push(socket.playercastName); 132 | socket.emit('invalid', false); 133 | events.emit('playercast-added', socket.playercastName); 134 | } 135 | else 136 | { 137 | socket.playercastInvalid = true; 138 | socket.emit('invalid', 'name'); 139 | } 140 | }); 141 | 142 | socket.on('playercast-ctl', msg => 143 | { 144 | switch(msg) 145 | { 146 | case 'track-ended': 147 | if(!controller.checkNextTrack()) 148 | controller.webControl('STOP'); 149 | break; 150 | case 'previous-track': 151 | controller.webControl('SKIP-'); 152 | break; 153 | case 'next-track': 154 | controller.webControl('SKIP+'); 155 | break; 156 | default: 157 | break; 158 | } 159 | }); 160 | 161 | socket.on('status-update', bridge.setGnomeStatus); 162 | socket.on('show-remote', msg => 163 | { 164 | if(msg) 165 | controller.setSlideshow(); 166 | else 167 | { 168 | controller.clearSlideshow(); 169 | controller.slideshow = false; 170 | } 171 | 172 | bridge.setGnomeRemote(msg) 173 | }); 174 | 175 | socket.on('disconnect', () => 176 | { 177 | if(!socket.playercastName) 178 | return checkClients(); 179 | 180 | if( 181 | socket.playercastInvalid 182 | || !exports.playercasts.includes(socket.playercastName) 183 | ) 184 | return; 185 | 186 | var index = exports.playercasts.indexOf(socket.playercastName); 187 | exports.playercasts.splice(index, 1); 188 | }); 189 | } 190 | 191 | function initWebPlayer() 192 | { 193 | var initType = ( 194 | bridge.selection.streamType === 'MUSIC' 195 | && !bridge.config.musicVisualizer 196 | ) ? 'MUSIC' : 'VIDEO'; 197 | 198 | var isSub = ( 199 | bridge.selection.streamType !== 'MUSIC' 200 | && bridge.selection.streamType !== 'PICTURE' 201 | && (bridge.selection.subsPath || bridge.selection.subsSrc) 202 | && (bridge.selection.streamType === 'VIDEO' 203 | || bridge.selection.streamType === 'VIDEO_AENC' 204 | || !bridge.config.burnSubtitles) 205 | ); 206 | 207 | var webData = { 208 | type: initType, 209 | subs: isSub, 210 | i18n: { 211 | speed: gettext.translate(messages.plyr.speed), 212 | normal: gettext.translate(messages.plyr.normal) 213 | } 214 | } 215 | 216 | websocket.emit('webplayer-init', webData); 217 | 218 | if(!gnome.isRemote) 219 | bridge.setGnomeRemote(true); 220 | } 221 | 222 | function checkClients() 223 | { 224 | if(exports.activeConnections > 0) 225 | exports.activeConnections--; 226 | 227 | clientTimeout = setTimeout(() => 228 | { 229 | clientTimeout = null; 230 | 231 | if(exports.activeConnections == 0) 232 | { 233 | controller.clearSlideshow(); 234 | controller.slideshow = false; 235 | bridge.setGnomeRemote(false); 236 | } 237 | }, 2500); 238 | } 239 | 240 | function sendMessage() 241 | { 242 | if(bridge.config.receiverType == 'chromecast') 243 | websocket.emit('message-refresh', gettext.translate(messages.receiverChromecast)); 244 | else if(bridge.config.receiverType == 'playercast') 245 | websocket.emit('message-refresh', gettext.translate(messages.receiverPlayercast)); 246 | else if(!bridge.selection.filePath) 247 | websocket.emit('message-refresh', gettext.translate(messages.noMedia)); 248 | else if(encode.streamProcess) 249 | websocket.emit('message-refresh', gettext.translate(messages.streamActive)); 250 | else if(exports.activeConnections > 1) 251 | websocket.emit('message-refresh', gettext.translate(messages.connectLimit)); 252 | else if(exports.activeConnections == 1) 253 | exports.activeConnections--; 254 | else 255 | websocket.emit('message-clear'); 256 | } 257 | -------------------------------------------------------------------------------- /node_scripts/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const bowser = require('bowser'); 4 | const path = require('path'); 5 | const extract = require('ffmpeg-extract'); 6 | const bridge = require('./bridge'); 7 | const webcreator = require('./web-creator'); 8 | const sender = require('./sender'); 9 | const socket = require('./server-socket'); 10 | const encode = require('./encode'); 11 | const gettext = require('./gettext'); 12 | 13 | const app = express(); 14 | 15 | var userAgent = null; 16 | var server = app.listen(bridge.config.listeningPort, () => 17 | { 18 | gettext.initTranslations(); 19 | sender.configure(bridge.config.internalPort); 20 | socket.listen(server); 21 | 22 | bridge.createTempDir(() => socket.connectWs()); 23 | }); 24 | 25 | app.use(bodyParser.json()); 26 | 27 | exports.changePort = function(port) 28 | { 29 | server.close(); 30 | 31 | server = app.listen(port); 32 | socket.listen(server); 33 | } 34 | 35 | function checkMessagePage(req, res) 36 | { 37 | if( 38 | bridge.config.receiverType != 'other' 39 | || !bridge.selection.filePath 40 | || encode.streamProcess 41 | || socket.activeConnections > 0 42 | ) { 43 | res.sendFile(path.join(__dirname + '/../webplayer/message.html')); 44 | return true; 45 | } 46 | 47 | if(extract.video.subsProcess || extract.music.coverProcess) 48 | { 49 | res.sendFile(path.join(__dirname + '/../webplayer/loading.html')); 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | function getBrowserName() 57 | { 58 | if(!userAgent) 59 | return null; 60 | 61 | var parsedAgent = bowser.parse(userAgent); 62 | 63 | if(parsedAgent && parsedAgent.browser && parsedAgent.browser.name) 64 | return parsedAgent.browser.name; 65 | 66 | return null; 67 | } 68 | 69 | app.get('/', function(req, res) 70 | { 71 | var lang = req.acceptsLanguages.apply(req, gettext.locales); 72 | 73 | if(lang) gettext.setLocale(lang); 74 | else gettext.setLocale('en'); 75 | 76 | var isMessage = checkMessagePage(req, res); 77 | if(isMessage) return; 78 | 79 | if( 80 | bridge.config.receiverType === 'other' 81 | && userAgent !== req.headers['user-agent'] 82 | ) { 83 | userAgent = req.headers['user-agent']; 84 | sender.sendBrowserName(getBrowserName()); 85 | } 86 | 87 | switch(bridge.selection.streamType) 88 | { 89 | case 'VIDEO': 90 | res.sendFile(path.join(__dirname + '/../webplayer/webplayer_direct.html')); 91 | break; 92 | case 'MUSIC': 93 | if(bridge.config.musicVisualizer) 94 | res.sendFile(path.join(__dirname + '/../webplayer/webplayer_encode.html')); 95 | else 96 | res.sendFile(path.join(__dirname + '/../webplayer/webplayer_direct.html')); 97 | break; 98 | case 'PICTURE': 99 | res.sendFile(path.join(__dirname + '/../webplayer/picture.html')); 100 | break; 101 | default: 102 | res.sendFile(path.join(__dirname + '/../webplayer/webplayer_encode.html')); 103 | } 104 | }); 105 | 106 | app.get('/cast', function(req, res) 107 | { 108 | if(bridge.selection.addon) 109 | { 110 | /* Send to add-on if available, otherwise ignore request */ 111 | if(bridge.addon) 112 | bridge.addon.fileStream(req, res, bridge.selection, bridge.config); 113 | 114 | return; 115 | } 116 | 117 | switch(bridge.selection.streamType) 118 | { 119 | case 'MUSIC': 120 | if(bridge.config.musicVisualizer) 121 | webcreator.encodedStream(req, res); 122 | else 123 | webcreator.fileStream(req, res); 124 | break; 125 | case 'VIDEO': 126 | case 'PICTURE': 127 | webcreator.fileStream(req, res); 128 | break; 129 | default: 130 | webcreator.encodedStream(req, res); 131 | break; 132 | } 133 | }); 134 | 135 | app.get('/subs(webplayer)?', function(req, res) 136 | { 137 | if(bridge.selection.addon && bridge.selection.subsSrc) 138 | bridge.addon.subsStream(req, res, bridge.selection, bridge.config); 139 | else 140 | webcreator.subsStream(req, res); 141 | }); 142 | 143 | app.get('/cover', function(req, res) 144 | { 145 | if(bridge.selection.addon && bridge.selection.coverSrc) 146 | bridge.addon.coverStream(req, res, bridge.selection, bridge.config); 147 | else 148 | webcreator.coverStream(req, res); 149 | }); 150 | 151 | app.get('/webplayer/webconfig.css', function(req, res) 152 | { 153 | webcreator.webConfig(req, res); 154 | }); 155 | 156 | app.get('/api/*', function(req, res) 157 | { 158 | if(req.params[0] === 'browser') 159 | res.send({ name: getBrowserName() }); 160 | else 161 | webcreator.apiGet(req.params[0], req, res); 162 | }); 163 | 164 | app.post('/api/*', function(req, res) 165 | { 166 | webcreator.apiPost(req.params[0], req, res); 167 | }); 168 | 169 | app.get('/segment*', function(req, res) 170 | { 171 | webcreator.hlsStream(req, res); 172 | }); 173 | 174 | app.use('/webplayer', express.static(__dirname + '/../webplayer')); 175 | app.use('/plyr', express.static(__dirname + '/../node_modules/plyr/dist')); 176 | 177 | app.get('/*', function(req, res) 178 | { 179 | res.redirect('/'); 180 | }); 181 | -------------------------------------------------------------------------------- /node_scripts/utils/install.js: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | //bin/false || exec "$(command -v nodejs || command -v node)" "$0" 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const { execSync } = require('child_process'); 7 | 8 | const extensionsPath = path.join(__dirname + '/../../..'); 9 | 10 | /* Add-ons should be installed synchronously */ 11 | var extensions = fs.readdirSync(extensionsPath); 12 | 13 | extensions.forEach(folder => 14 | { 15 | if(folder.startsWith('cast-to-tv') && folder.includes('addon@')) 16 | { 17 | var addonFolder = path.join(extensionsPath, folder); 18 | var addonName = folder.substring(11, folder.lastIndexOf('-')); 19 | var isPackage = fs.existsSync(addonFolder + '/package.json'); 20 | 21 | if(isPackage) 22 | { 23 | var addonText = addonName[0].toUpperCase() + addonName.slice(1); 24 | var installText = `Installing: Cast to TV - ${addonText} Add-on`; 25 | var textLength = installText.length; 26 | 27 | while(textLength) 28 | { 29 | process.stdout.write('-'); 30 | textLength--; 31 | } 32 | 33 | process.stdout.write('\n'); 34 | console.log(installText); 35 | execSync('npm install', { cwd: addonFolder, stdio: 'inherit' }); 36 | process.stdout.write('\n'); 37 | } 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /node_scripts/utils/local-ip.js: -------------------------------------------------------------------------------- 1 | const internalIp = require('internal-ip').v4; 2 | const ip = internalIp.sync(); 3 | if(ip) console.log(ip); 4 | -------------------------------------------------------------------------------- /node_scripts/utils/scanner.js: -------------------------------------------------------------------------------- 1 | const scanner = require('multicast-scanner'); 2 | const gnome = require('../gnome'); 3 | const SERVICE = (process.argv[2]) ? process.argv[2] : 'googlecast'; 4 | 5 | var opts = { 6 | service_name: `_${SERVICE}._tcp.local`, 7 | full_scan: true 8 | }; 9 | 10 | scanner(opts, (err, devices) => 11 | { 12 | var results = []; 13 | 14 | if(!err) 15 | { 16 | devices.forEach(device => 17 | { 18 | results.push({ 19 | name: device.name, 20 | friendlyName: device.friendlyName, 21 | ip: '' 22 | }); 23 | }); 24 | } 25 | 26 | const devName = (SERVICE !== 'googlecast') ? SERVICE : 'chromecast'; 27 | gnome.setSetting(`${devName}-devices`, JSON.stringify(results)); 28 | }); 29 | 30 | gnome.loadSchema(); 31 | -------------------------------------------------------------------------------- /node_scripts/utils/vttextract.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const extract = require('ffmpeg-extract'); 6 | const remove = require('../remove'); 7 | const gnome = require('../gnome'); 8 | const metadata = require('../../metadata'); 9 | 10 | var opts = { 11 | ffprobePath: null, 12 | inPath: null, 13 | outDir: null, 14 | quiet: false, 15 | recursive: false 16 | }; 17 | 18 | var config = {}; 19 | var currOutPath = null; 20 | var filesCount = 0; 21 | 22 | function showHelp() 23 | { 24 | const version = (metadata.git) ? `git-${metadata.git}` : `v${metadata.version}`; 25 | 26 | console.log([ 27 | ``, 28 | `vttextract - Cast to TV ${version} subtitles extractor`, 29 | ``, 30 | `Usage: vttextract [OPTIONS]`, 31 | ``, 32 | ` source - can be path to single video file or dir with videos`, 33 | ``, 34 | `OPTIONS:`, 35 | ` -q, --quiet Do not print extraction info except errors`, 36 | ` -r, --recursive Extract from files in subdirectories`, 37 | `` 38 | ].join('\n')); 39 | } 40 | 41 | function logInfo(text) 42 | { 43 | if(!opts.quiet) 44 | console.log(text); 45 | } 46 | 47 | function writeProgress(fileName, mark, isEndLine) 48 | { 49 | mark = mark || '\u2B58'; 50 | 51 | process.stdout.cursorTo(0); 52 | process.stdout.clearLine(0); 53 | process.stdout.write(`${mark} ${fileName}`); 54 | 55 | if(isEndLine) 56 | process.stdout.write('\n'); 57 | } 58 | 59 | function parseArgs() 60 | { 61 | for(var i = 2; i < process.argv.length; i++) 62 | { 63 | switch(process.argv[i]) 64 | { 65 | case '-q': 66 | case '--quiet': 67 | opts.quiet = true; 68 | break; 69 | case '-r': 70 | case '--recursive': 71 | opts.recursive = true; 72 | break; 73 | case '-h': 74 | case '--help': 75 | return false; 76 | default: 77 | if(!opts.inPath) 78 | opts.inPath = process.argv[i]; 79 | else 80 | return false; 81 | break; 82 | } 83 | } 84 | 85 | return (opts.inPath !== null); 86 | } 87 | 88 | function extractFromDir(dirPath) 89 | { 90 | logInfo(`Browsing dir: "${dirPath}"`); 91 | 92 | return new Promise((resolve, reject) => 93 | { 94 | fs.readdir(dirPath, async (err, files) => 95 | { 96 | if(err) return reject(err); 97 | 98 | for(var fileName of files) 99 | { 100 | var runPath = path.join(dirPath, fileName); 101 | await runInPath(runPath, opts.recursive).catch(onReject); 102 | } 103 | 104 | resolve(); 105 | }); 106 | }); 107 | } 108 | 109 | function extractFromFile(filePath) 110 | { 111 | return new Promise((resolve, reject) => 112 | { 113 | var ffprobeOpts = { 114 | ffprobePath : opts.ffprobePath, 115 | filePath: filePath 116 | }; 117 | 118 | extract.analyzeFile(ffprobeOpts, (err, data) => 119 | { 120 | if(err) 121 | { 122 | /* Returns process error when run on non-video file */ 123 | if(err.message.includes('FFprobe process error')) 124 | return resolve(); 125 | else 126 | return reject(err); 127 | } 128 | 129 | if(!extract.video.getIsSubsMerged(data)) 130 | return resolve(); 131 | 132 | var parsed = path.parse(filePath); 133 | 134 | /* 135 | Access is done here, so we disable second 136 | access test with overwrite set to true 137 | */ 138 | var extOpts = { 139 | file: filePath, 140 | outPath: path.join(opts.outDir, parsed.name + '.vtt'), 141 | overwrite: true, 142 | vttparser: true 143 | }; 144 | 145 | if(data.streams.length > 3) 146 | { 147 | extOpts.streamIndex = extract.video.getSubsTrackIndex( 148 | data, config.subsPreferred 149 | ); 150 | 151 | if(!extOpts.streamIndex && config.subsFallback) 152 | { 153 | extOpts.streamIndex = extract.video.getSubsTrackIndex( 154 | data, config.subsFallback 155 | ); 156 | } 157 | } 158 | 159 | fs.access(extOpts.outPath, fs.constants.F_OK, (err) => 160 | { 161 | if(!err) return resolve(); 162 | 163 | var fileName = parsed.name + parsed.ext; 164 | 165 | if(!opts.quiet) 166 | writeProgress(fileName); 167 | 168 | currOutPath = extOpts.outPath; 169 | 170 | extract.video.videoToVtt(extOpts, (err) => 171 | { 172 | currOutPath = null; 173 | 174 | if(err) 175 | { 176 | writeProgress(fileName, '\u2716', true); 177 | return remove.file(extOpts.outPath, () => reject(err)); 178 | } 179 | 180 | if(!opts.quiet) 181 | writeProgress(fileName, '\u2714', true); 182 | 183 | filesCount++; 184 | resolve(); 185 | }); 186 | }); 187 | }); 188 | }); 189 | } 190 | 191 | function runInPath(runPath, isRecursive) 192 | { 193 | return new Promise((resolve, reject) => 194 | { 195 | fs.access(runPath, fs.constants.F_OK, (err) => 196 | { 197 | if(err) return reject(err); 198 | 199 | fs.stat(runPath, async (err, stat) => 200 | { 201 | if(err) return reject(err); 202 | 203 | if(stat.isDirectory()) 204 | { 205 | if(isRecursive) 206 | await extractFromDir(runPath).catch(onReject); 207 | } 208 | else if(stat.isFile()) 209 | await extractFromFile(runPath).catch(onReject); 210 | else 211 | return reject(new Error(`Not a file or dir: ${runPath}`)); 212 | 213 | resolve(); 214 | }); 215 | }); 216 | }); 217 | } 218 | 219 | function onReject(err) 220 | { 221 | console.error(err.message); 222 | } 223 | 224 | function onFinish(hideMsg) 225 | { 226 | if(hideMsg) 227 | console.log(); 228 | else if(filesCount > 0) 229 | { 230 | var text = `Extracted subtitles from ${filesCount} file`; 231 | 232 | if(filesCount > 1) 233 | text += 's'; 234 | 235 | logInfo(text); 236 | } 237 | else 238 | logInfo('No subtitles extracted'); 239 | 240 | exitWithCode(0); 241 | } 242 | 243 | function onUncaughtException(err) 244 | { 245 | if(err) 246 | console.error(err.message); 247 | 248 | exitWithCode(1); 249 | } 250 | 251 | function exitWithCode(code) 252 | { 253 | if(currOutPath) 254 | remove.file(currOutPath, () => process.exit(code)); 255 | else 256 | process.exit(code); 257 | } 258 | 259 | function startExtract() 260 | { 261 | const argsOk = parseArgs(); 262 | 263 | if(!argsOk) 264 | return showHelp(); 265 | 266 | gnome.loadSchema(); 267 | 268 | if(!gnome.getBoolean('extractor-reuse')) 269 | return console.error('Reusing subtitles is disabled'); 270 | 271 | opts.outDir = gnome.getSetting('extractor-dir'); 272 | 273 | if(!opts.outDir) 274 | return console.error('Could not obtain save dir setting'); 275 | 276 | opts.ffprobePath = gnome.getSetting('ffprobe-path') || '/usr/bin/ffprobe'; 277 | config.subsPreferred = gnome.getSetting('subs-preferred') || 'eng/English'; 278 | config.subsFallback = gnome.getSetting('subs-fallback'); 279 | 280 | process.on('SIGINT', () => onFinish(true)); 281 | process.on('SIGTERM', () => onFinish(true)); 282 | process.on('uncaughtException', onUncaughtException); 283 | 284 | /* First run must be recursive to support starting in dir */ 285 | runInPath(opts.inPath, true) 286 | .then(onFinish) 287 | .catch(onReject) 288 | } 289 | 290 | startExtract(); 291 | -------------------------------------------------------------------------------- /node_scripts/web-creator.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const debug = require('debug')('web-creator'); 4 | const bridge = require('./bridge'); 5 | const socket = require('./server-socket'); 6 | const encode = require('./encode'); 7 | const shared = require('../shared'); 8 | 9 | exports.fileStream = function(req, res) 10 | { 11 | res.setHeader('Access-Control-Allow-Origin', '*'); 12 | 13 | var streamType = bridge.selection.streamType; 14 | var filePath = bridge.selection.filePath; 15 | 16 | if(!filePath) 17 | { 18 | debug('No file path'); 19 | return res.sendStatus(404); 20 | } 21 | 22 | /* Check if file exists */ 23 | fs.access(filePath, fs.constants.F_OK, (err) => 24 | { 25 | if(err) 26 | { 27 | debug(err); 28 | return res.sendStatus(404); 29 | } 30 | 31 | return res.sendFile(filePath); 32 | }); 33 | } 34 | 35 | exports.encodedStream = function(req, res) 36 | { 37 | if(!encode.enabled) 38 | return res.sendStatus(204); 39 | 40 | var filePath = bridge.selection.filePath; 41 | 42 | if(!filePath) 43 | return res.sendStatus(404); 44 | 45 | /* Prevent spawning more then one ffmpeg encode process */ 46 | if(encode.streamProcess) 47 | return res.sendStatus(429); 48 | 49 | var streamType = bridge.selection.streamType; 50 | 51 | /* Check if file exists */ 52 | fs.access(filePath, fs.constants.F_OK, (err) => 53 | { 54 | if(err) return res.sendStatus(404); 55 | 56 | res.writeHead(200, { 57 | 'Access-Control-Allow-Origin': '*', 58 | 'Connection': 'close', 59 | 'Content-Type': 'video/mp4' 60 | }); 61 | 62 | switch(streamType) 63 | { 64 | case 'MUSIC': 65 | encode.musicVisualizer().pipe(res); 66 | break; 67 | case 'VIDEO_VENC': 68 | case 'VIDEO_VENC_AENC': 69 | case 'VIDEO_AENC_VENC': 70 | var audioEnc = streamType.includes('_AENC'); 71 | switch(bridge.config.videoAcceleration) 72 | { 73 | case 'vaapi': 74 | encode.videoVaapi(audioEnc).pipe(res); 75 | break; 76 | case 'nvenc': 77 | encode.videoNvenc(audioEnc).pipe(res); 78 | break; 79 | case 'amf': 80 | encode.videoAmf(audioEnc).pipe(res); 81 | break; 82 | default: 83 | encode.video(audioEnc).pipe(res); 84 | break; 85 | } 86 | break; 87 | case 'VIDEO_AENC': 88 | encode.audio().pipe(res); 89 | break; 90 | default: 91 | encode.video().pipe(res); 92 | break; 93 | } 94 | 95 | res.once('close', encode.closeStreamProcess); 96 | }); 97 | } 98 | 99 | exports.subsStream = function(req, res) 100 | { 101 | res.setHeader('Access-Control-Allow-Origin', '*'); 102 | 103 | if(!bridge.selection.streamType.startsWith('VIDEO')) 104 | return res.sendStatus(204); 105 | 106 | var subsPath = bridge.selection.subsPath; 107 | 108 | /* Check if file is specified and exists */ 109 | if(!subsPath) 110 | return res.sendStatus(204); 111 | 112 | if( 113 | req._parsedUrl.pathname === '/subswebplayer' 114 | && !subsPath.endsWith('.vtt') 115 | ) { 116 | return res.sendStatus(204); 117 | } 118 | 119 | fs.access(subsPath, fs.constants.F_OK, (err) => 120 | { 121 | if(err) return res.sendStatus(404); 122 | 123 | return res.sendFile(subsPath); 124 | }); 125 | } 126 | 127 | exports.coverStream = function(req, res) 128 | { 129 | res.setHeader('Access-Control-Allow-Origin', '*'); 130 | 131 | if(bridge.selection.streamType !== 'MUSIC') 132 | return res.sendStatus(204); 133 | 134 | var coverPath = bridge.mediaData.coverPath; 135 | 136 | if(!coverPath) 137 | return res.sendStatus(204); 138 | 139 | fs.access(coverPath, fs.constants.F_OK, (err) => 140 | { 141 | if(err) return res.sendStatus(404); 142 | 143 | return res.sendFile(coverPath); 144 | }); 145 | } 146 | 147 | exports.hlsStream = function(req, res) 148 | { 149 | res.setHeader('Access-Control-Allow-Origin', '*'); 150 | 151 | var filePath = shared.hlsDir + req.url; 152 | 153 | /* Check if stream segment exists */ 154 | fs.access(filePath, fs.constants.F_OK, (err) => 155 | { 156 | if(err) 157 | { 158 | debug(err); 159 | return res.sendStatus(404); 160 | } 161 | 162 | debug(`Send HLS segment: ${req.url}`); 163 | return res.sendFile(filePath); 164 | }); 165 | } 166 | 167 | exports.webConfig = function(req, res) 168 | { 169 | res.setHeader('Access-Control-Allow-Origin', '*'); 170 | res.setHeader('Content-Type', 'text/css'); 171 | 172 | var size = bridge.config.webplayerSubs; 173 | 174 | var webConfig = `@media { 175 | .plyr__captions{font-size:${5*size}vmin} 176 | .plyr:-webkit-full-screen .plyr__captions{font-size:${5*size}vmin} 177 | .plyr:-moz-full-screen .plyr__captions{font-size:${5*size}vmin} 178 | .plyr:-ms-fullscreen .plyr__captions{font-size:${5*size}vmin} 179 | .plyr:fullscreen .plyr__captions{font-size:${5*size}vmin}\n}` 180 | 181 | res.send(webConfig); 182 | } 183 | 184 | exports.apiGet = function(type, req, res) 185 | { 186 | switch(type) 187 | { 188 | case 'config': 189 | case 'selection': 190 | case 'playlist': 191 | case 'status': 192 | res.send(bridge[type]); 193 | break; 194 | case 'playercasts': 195 | res.send(socket.playercasts); 196 | break; 197 | case 'playback-data': 198 | res.send(bridge.getPlaybackData()); 199 | break; 200 | case 'remote-buttons': 201 | res.send(bridge.getRemoteButtons()); 202 | break; 203 | case 'is-enabled': 204 | res.send({ isEnabled: true }); 205 | break; 206 | case 'media-data': 207 | res.send(bridge.mediaData); 208 | break; 209 | default: 210 | res.sendStatus(404); 211 | break; 212 | } 213 | } 214 | 215 | exports.apiPost = function(type, req, res) 216 | { 217 | switch(type) 218 | { 219 | case 'config': 220 | bridge.updateConfig(req.body); 221 | res.sendStatus(200); 222 | break; 223 | case 'selection': 224 | bridge.updateSelection(req.body); 225 | res.sendStatus(200); 226 | break; 227 | case 'playlist': 228 | const append = (req.query && req.query.append === 'true'); 229 | bridge.updatePlaylist(req.body, append); 230 | res.sendStatus(200); 231 | break; 232 | case 'remote': 233 | bridge.updateRemote(req.body); 234 | res.sendStatus(200); 235 | break; 236 | case 'playback-data': 237 | bridge.updatePlaylist(req.body.playlist, false); 238 | bridge.updateSelection(req.body.selection); 239 | res.sendStatus(200); 240 | break; 241 | case 'lock-screen': 242 | bridge.updateLockScreen(req.body); 243 | res.sendStatus(200); 244 | break; 245 | default: 246 | res.sendStatus(404); 247 | break; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cast-to-tv", 3 | "description": "Cast files to Chromecast, web browser or media player app over local network.", 4 | "main": "node_scripts/server.js", 5 | "author": "Rafostar", 6 | "license": "GPL-2.0", 7 | "dependencies": { 8 | "body-parser": "^1.19.0", 9 | "bowser": "^2.9.0", 10 | "chromecast-controller": "^0.3.1", 11 | "debug": "^4.1.1", 12 | "express": "^4.17.1", 13 | "ffmpeg-extract": "^1.1.2", 14 | "gettext-parser": "^3.1.0", 15 | "internal-ip": "^4.3.0", 16 | "multicast-scanner": "^2.3.0", 17 | "node-gettext": "^2.0.0", 18 | "plyr": "3.5.3", 19 | "socket.io": "^2.3.0", 20 | "ws": "^7.2.1" 21 | }, 22 | "scripts": { 23 | "install": "sh node_scripts/utils/install.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/Rafostar/gnome-shell-extension-cast-to-tv.git" 28 | }, 29 | "keywords": [ 30 | "gnome-shell-extension", 31 | "chromecast", 32 | "media", 33 | "video", 34 | "stream", 35 | "cast" 36 | ], 37 | "bugs": { 38 | "url": "https://github.com/Rafostar/gnome-shell-extension-cast-to-tv/issues" 39 | }, 40 | "homepage": "https://github.com/Rafostar/gnome-shell-extension-cast-to-tv", 41 | "devDependencies": { 42 | "bufferutil": "^4.0.1", 43 | "utf-8-validate": "^5.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Cast to TV - Links Addon package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Cast to TV - Links Addon\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 21 | #: app.js:140 22 | msgid "No Image" 23 | msgstr "" 24 | 25 | #: app.js:152 26 | msgid "Title:" 27 | msgstr "" 28 | 29 | #: app.js:153 30 | msgid "Format:" 31 | msgstr "" 32 | 33 | #: app.js:154 34 | msgid "Resolution:" 35 | msgstr "" 36 | 37 | #: app.js:155 38 | msgid "Video codec:" 39 | msgstr "" 40 | 41 | #: app.js:156 42 | msgid "Audio codec:" 43 | msgstr "" 44 | 45 | #: app.js:157 46 | msgid "Subtitles:" 47 | msgstr "" 48 | 49 | #: app.js:173 50 | msgid "Enter or drop link here" 51 | msgstr "" 52 | 53 | #: app.js:182 54 | msgid "Cast link" 55 | msgstr "" 56 | 57 | #: widget.js:18 58 | msgid "Link" 59 | msgstr "" 60 | 61 | #: links_prefs.js:31 62 | msgid "Links" 63 | msgstr "" 64 | 65 | #: links_prefs.js:36 66 | msgid "Links Options" 67 | msgstr "" 68 | 69 | #: links_prefs.js:40 70 | msgid "Preferred format" 71 | msgstr "" 72 | 73 | #: links_prefs.js:42 74 | msgid "Best seekable" 75 | msgstr "" 76 | 77 | #: links_prefs.js:43 78 | msgid "Best quality" 79 | msgstr "" 80 | 81 | #: links_prefs.js:43 82 | msgid "(experimental)" 83 | msgstr "" 84 | 85 | #: links_prefs.js:49 86 | msgid "Path to youtube-dl" 87 | msgstr "" 88 | 89 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 90 | #: links_prefs.js:58 91 | msgid "Max video quality" 92 | msgstr "" 93 | 94 | #: links_prefs.js:76 95 | msgid "Allow VP9 codec" 96 | msgstr "" 97 | 98 | #: links_prefs.js:84 99 | msgid "Subtitles" 100 | msgstr "" 101 | 102 | #: links_prefs.js:88 103 | msgid "Preferred language" 104 | msgstr "" 105 | 106 | #: links_prefs.js:96 107 | msgid "Fallback language" 108 | msgstr "" 109 | 110 | #: links_prefs.js:98 111 | msgid "none" 112 | msgstr "" 113 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/de.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: German\n" 9 | "Language: de_DE\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: de\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Kein Bild" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "Titel:" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "Format:" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "Auflösung:" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "Videocodec:" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "Audio Codec:" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "Untertitel:" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "Link hier eingeben oder ablegen" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "Castlink" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "Link" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "Links" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "Linkoptionen" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "Bevorzugtes Format" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "Am besten spulbar" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "Beste Qualität" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "(experimentell)" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "Pfad zu youtube-dl" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "Max. Videoqualität" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "VP9 Codec erlauben" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "Untertitel" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "Bevorzugte Sprache" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "Fallback-Sprache" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "keine" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/en_GB.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: English, United Kingdom\n" 9 | "Language: en_GB\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: en-GB\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Spanish\n" 9 | "Language: es_ES\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: es-ES\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Sin Imagen" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "Título:" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "Formato:" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "Resolución:" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "Códec de video:" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "Códec de audio:" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "Subtitulos:" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "Ingrese o arrastre la dirección acá" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "Emitir enlace" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "Enlace" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "Enlaces" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "Opciones del enlace" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "Formato preferido" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "Mejor busqueda" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "Mejor calidad" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "(experimental)" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "Ruta a youtube-dl" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "Max calidad de video" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "Permitir el códec VP9" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "Subtitulos" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "Lenguage preferido" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "Lenguaje de soporte" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "ninguno" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/fo.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Faroese\n" 9 | "Language: fo_FO\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: fo\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/fr.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-06-25 14:05\n" 7 | "Last-Translator: \n" 8 | "Language-Team: French\n" 9 | "Language: fr_FR\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Project-ID: 356831\n" 16 | "X-Crowdin-Language: fr\n" 17 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 18 | "X-Crowdin-File-ID: 26\n" 19 | 20 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 21 | #: app.js:140 22 | msgid "No Image" 23 | msgstr "" 24 | 25 | #: app.js:152 26 | msgid "Title:" 27 | msgstr "" 28 | 29 | #: app.js:153 30 | msgid "Format:" 31 | msgstr "" 32 | 33 | #: app.js:154 34 | msgid "Resolution:" 35 | msgstr "" 36 | 37 | #: app.js:155 38 | msgid "Video codec:" 39 | msgstr "" 40 | 41 | #: app.js:156 42 | msgid "Audio codec:" 43 | msgstr "" 44 | 45 | #: app.js:157 46 | msgid "Subtitles:" 47 | msgstr "" 48 | 49 | #: app.js:173 50 | msgid "Enter or drop link here" 51 | msgstr "" 52 | 53 | #: app.js:182 54 | msgid "Cast link" 55 | msgstr "" 56 | 57 | #: widget.js:18 58 | msgid "Link" 59 | msgstr "" 60 | 61 | #: links_prefs.js:31 62 | msgid "Links" 63 | msgstr "" 64 | 65 | #: links_prefs.js:36 66 | msgid "Links Options" 67 | msgstr "" 68 | 69 | #: links_prefs.js:40 70 | msgid "Preferred format" 71 | msgstr "" 72 | 73 | #: links_prefs.js:42 74 | msgid "Best seekable" 75 | msgstr "" 76 | 77 | #: links_prefs.js:43 78 | msgid "Best quality" 79 | msgstr "" 80 | 81 | #: links_prefs.js:43 82 | msgid "(experimental)" 83 | msgstr "" 84 | 85 | #: links_prefs.js:49 86 | msgid "Path to youtube-dl" 87 | msgstr "" 88 | 89 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 90 | #: links_prefs.js:58 91 | msgid "Max video quality" 92 | msgstr "" 93 | 94 | #: links_prefs.js:76 95 | msgid "Allow VP9 codec" 96 | msgstr "" 97 | 98 | #: links_prefs.js:84 99 | msgid "Subtitles" 100 | msgstr "" 101 | 102 | #: links_prefs.js:88 103 | msgid "Preferred language" 104 | msgstr "" 105 | 106 | #: links_prefs.js:96 107 | msgid "Fallback language" 108 | msgstr "" 109 | 110 | #: links_prefs.js:98 111 | msgid "none" 112 | msgstr "" 113 | 114 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/it.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Italian\n" 9 | "Language: it_IT\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: it\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Nessuna\n" 22 | "immagine" 23 | 24 | #: app.js:152 25 | msgid "Title:" 26 | msgstr "Titolo:" 27 | 28 | #: app.js:153 29 | msgid "Format:" 30 | msgstr "Formato:" 31 | 32 | #: app.js:154 33 | msgid "Resolution:" 34 | msgstr "Risoluzione:" 35 | 36 | #: app.js:155 37 | msgid "Video codec:" 38 | msgstr "Codec video:" 39 | 40 | #: app.js:156 41 | msgid "Audio codec:" 42 | msgstr "Codec audio:" 43 | 44 | #: app.js:157 45 | msgid "Subtitles:" 46 | msgstr "Sottotitoli:" 47 | 48 | #: app.js:173 49 | msgid "Enter or drop link here" 50 | msgstr "Inserisci o rilascia link qui" 51 | 52 | #: app.js:182 53 | msgid "Cast link" 54 | msgstr "Link cast" 55 | 56 | #: widget.js:18 57 | msgid "Link" 58 | msgstr "Link" 59 | 60 | #: links_prefs.js:31 61 | msgid "Links" 62 | msgstr "Links" 63 | 64 | #: links_prefs.js:36 65 | msgid "Links Options" 66 | msgstr "Opzioni link" 67 | 68 | #: links_prefs.js:40 69 | msgid "Preferred format" 70 | msgstr "Formato preferito" 71 | 72 | #: links_prefs.js:42 73 | msgid "Best seekable" 74 | msgstr "Migliore ricercabile" 75 | 76 | #: links_prefs.js:43 77 | msgid "Best quality" 78 | msgstr "Migliore qualità" 79 | 80 | #: links_prefs.js:43 81 | msgid "(experimental)" 82 | msgstr "(sperimentale)" 83 | 84 | #: links_prefs.js:49 85 | msgid "Path to youtube-dl" 86 | msgstr "Percorso a youtube-dl" 87 | 88 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 89 | #: links_prefs.js:58 90 | msgid "Max video quality" 91 | msgstr "Massima qualità video" 92 | 93 | #: links_prefs.js:76 94 | msgid "Allow VP9 codec" 95 | msgstr "Consenti codec VP9" 96 | 97 | #: links_prefs.js:84 98 | msgid "Subtitles" 99 | msgstr "Sottotitoli" 100 | 101 | #: links_prefs.js:88 102 | msgid "Preferred language" 103 | msgstr "Lingua preferita" 104 | 105 | #: links_prefs.js:96 106 | msgid "Fallback language" 107 | msgstr "Lingua di Fallback" 108 | 109 | #: links_prefs.js:98 110 | msgid "none" 111 | msgstr "nessuna" 112 | 113 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/nl.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Dutch\n" 9 | "Language: nl_NL\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: nl\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Geen afbeelding" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "Titel:" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "Formaat:" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "Resolutie:" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "Videocodec:" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "Audiocodec:" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "Ondertiteling:" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "Voer een link in of versleep een link hierheen" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "Link casten" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "Link" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "Links" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "Link-instellingen" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "Voorkeursformaat" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "Best doorspoelbaar" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "Beste kwaliteit" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "(experimenteel)" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "Pad naar youtube-dl" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "Max. videokwaliteit" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "VP9-codec toestaan" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "Ondertiteling" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "Voorkeurstaal" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "Terugvallen op taal" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "geen" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/pl.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Polish\n" 9 | "Language: pl_PL\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: pl\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Brak obrazka" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "Tytuł:" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "Format:" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "Rozdzielczość:" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "Kodek wideo:" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "Kodek audio:" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "Napisy:" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "Wprowadź lub upuść link tutaj" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "Prześlij link" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "Link" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "Linki" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "Opcje linków" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "Preferowany format" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "Najlepszy z przewijaniem" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "Najlepsza jakość" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "(eksperymentalna)" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "Ścieżka do youtube-dl" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "Maksymalna jakość wideo" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "Dopuść kodek VP9" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "Napisy" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "Preferowany język" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "Drugorzędny język" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "brak" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/ro.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-21 10:05\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Romanian\n" 9 | "Language: ro_RO\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: ro\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv-links-addon/tr.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2019-10-15 20:55+0200\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Turkish\n" 9 | "Language: tr_TR\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: tr\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv-links-addon/cast-to-tv-links-addon.pot\n" 17 | 18 | #. TRANSLATORS: Make sure this text fits the black background. Otherwise the frame will get stretched!!! 19 | #: app.js:140 20 | msgid "No Image" 21 | msgstr "Görsel Yok" 22 | 23 | #: app.js:152 24 | msgid "Title:" 25 | msgstr "Başlık:" 26 | 27 | #: app.js:153 28 | msgid "Format:" 29 | msgstr "Biçim:" 30 | 31 | #: app.js:154 32 | msgid "Resolution:" 33 | msgstr "Çözünürlük:" 34 | 35 | #: app.js:155 36 | msgid "Video codec:" 37 | msgstr "Video çözücü:" 38 | 39 | #: app.js:156 40 | msgid "Audio codec:" 41 | msgstr "Ses çözücü:" 42 | 43 | #: app.js:157 44 | msgid "Subtitles:" 45 | msgstr "Altyazılar:" 46 | 47 | #: app.js:173 48 | msgid "Enter or drop link here" 49 | msgstr "Bağlantıyı buraya gir veya sürükle" 50 | 51 | #: app.js:182 52 | msgid "Cast link" 53 | msgstr "Akış bağlantısı" 54 | 55 | #: widget.js:18 56 | msgid "Link" 57 | msgstr "Bağlantı" 58 | 59 | #: links_prefs.js:31 60 | msgid "Links" 61 | msgstr "Bağlantılar" 62 | 63 | #: links_prefs.js:36 64 | msgid "Links Options" 65 | msgstr "Bağlantı Seçenekleri" 66 | 67 | #: links_prefs.js:40 68 | msgid "Preferred format" 69 | msgstr "Tercih edilen biçim" 70 | 71 | #: links_prefs.js:42 72 | msgid "Best seekable" 73 | msgstr "En iyi aranabilir" 74 | 75 | #: links_prefs.js:43 76 | msgid "Best quality" 77 | msgstr "En iyi kalite" 78 | 79 | #: links_prefs.js:43 80 | msgid "(experimental)" 81 | msgstr "(deneysel)" 82 | 83 | #: links_prefs.js:49 84 | msgid "Path to youtube-dl" 85 | msgstr "Eklenti yolu youtube-dl" 86 | 87 | #. TRANSLATORS: Can be translated simply as "Max quality" to make it shorter 88 | #: links_prefs.js:58 89 | msgid "Max video quality" 90 | msgstr "En iyi video kalitesi" 91 | 92 | #: links_prefs.js:76 93 | msgid "Allow VP9 codec" 94 | msgstr "VP9 çözücüye izin ver" 95 | 96 | #: links_prefs.js:84 97 | msgid "Subtitles" 98 | msgstr "Altyazılar" 99 | 100 | #: links_prefs.js:88 101 | msgid "Preferred language" 102 | msgstr "Tercih edilen dil" 103 | 104 | #: links_prefs.js:96 105 | msgid "Fallback language" 106 | msgstr "İkincil dil" 107 | 108 | #: links_prefs.js:98 109 | msgid "none" 110 | msgstr "hiçbiri" 111 | 112 | -------------------------------------------------------------------------------- /po/cast-to-tv/cast-to-tv.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Cast to TV package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Cast to TV\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #. TRANSLATORS: When "Cast Media" service is turned off 21 | #: widget.js:34 widget.js:111 22 | msgid "Cast Off" 23 | msgstr "" 24 | 25 | #: widget.js:39 file-chooser.js:80 26 | msgid "Video" 27 | msgstr "" 28 | 29 | #: widget.js:40 30 | msgid "Music" 31 | msgstr "" 32 | 33 | #: widget.js:41 34 | msgid "Picture" 35 | msgstr "" 36 | 37 | #: widget.js:42 widget.js:109 38 | msgid "Turn On" 39 | msgstr "" 40 | 41 | #: widget.js:43 42 | msgid "Cast Settings" 43 | msgstr "" 44 | 45 | #: widget.js:92 46 | msgid "Turn Off" 47 | msgstr "" 48 | 49 | #: widget.js:93 50 | msgid "Cast Media" 51 | msgstr "" 52 | 53 | #: widget.js:341 54 | msgid "Browser" 55 | msgstr "" 56 | 57 | #: playlist.js:344 58 | msgid "Playlist" 59 | msgstr "" 60 | 61 | #. TRANSLATORS: Button text when selected SINGLE file 62 | #: file-chooser.js:18 63 | msgid "Cast Selected File" 64 | msgstr "" 65 | 66 | #. TRANSLATORS: Button text when selected MULTIPLE files 67 | #: file-chooser.js:20 68 | msgid "Cast Selected Files" 69 | msgstr "" 70 | 71 | #: file-chooser.js:21 72 | msgid "Add to Playlist" 73 | msgstr "" 74 | 75 | #: file-chooser.js:77 76 | msgid "Transcode" 77 | msgstr "" 78 | 79 | #: file-chooser.js:81 80 | msgid "Audio" 81 | msgstr "" 82 | 83 | #: file-chooser.js:82 84 | msgid "Video + Audio" 85 | msgstr "" 86 | 87 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 88 | msgid "Automatic" 89 | msgstr "" 90 | 91 | #: file-chooser.js:270 92 | msgid "Cancel" 93 | msgstr "" 94 | 95 | #: file-chooser.js:279 96 | msgid "Add Subtitles" 97 | msgstr "" 98 | 99 | #: file-chooser.js:281 100 | msgid "Select Video" 101 | msgstr "" 102 | 103 | #: file-chooser.js:285 104 | msgid "Video Files" 105 | msgstr "" 106 | 107 | #: file-chooser.js:293 108 | msgid "Select Music" 109 | msgstr "" 110 | 111 | #: file-chooser.js:297 112 | msgid "Audio Files" 113 | msgstr "" 114 | 115 | #: file-chooser.js:305 116 | msgid "Select Picture" 117 | msgstr "" 118 | 119 | #: file-chooser.js:309 120 | msgid "Pictures" 121 | msgstr "" 122 | 123 | #: file-chooser.js:370 124 | msgid "Select Subtitles" 125 | msgstr "" 126 | 127 | #: file-chooser.js:374 128 | msgid "Subtitle Files" 129 | msgstr "" 130 | 131 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 132 | #: prefs.js:36 133 | msgid "is not installed" 134 | msgstr "" 135 | 136 | #: prefs.js:54 137 | msgid "Streaming in progress" 138 | msgstr "" 139 | 140 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 141 | #: prefs.js:64 142 | msgid "Stop media transfer before accessing extension settings" 143 | msgstr "" 144 | 145 | #: prefs.js:94 146 | msgid "Main Options" 147 | msgstr "" 148 | 149 | #: prefs.js:98 150 | msgid "Receiver type" 151 | msgstr "" 152 | 153 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 154 | #: prefs.js:102 prefs.js:659 155 | msgid "Playercast app" 156 | msgstr "" 157 | 158 | #. TRANSLATORS: Web browser or Media player app selection. 159 | #. This should be as short as possible e.g. "Browser | Player". 160 | #: prefs.js:105 161 | msgid "Web browser | Media player" 162 | msgstr "" 163 | 164 | #: prefs.js:110 165 | msgid "FFmpeg path" 166 | msgstr "" 167 | 168 | #: prefs.js:117 169 | msgid "FFprobe path" 170 | msgstr "" 171 | 172 | #: prefs.js:124 173 | msgid "Listening port" 174 | msgstr "" 175 | 176 | #: prefs.js:134 177 | msgid "Internal communication port" 178 | msgstr "" 179 | 180 | #: prefs.js:203 181 | msgid "Access web player from devices on local network" 182 | msgstr "" 183 | 184 | #: prefs.js:227 185 | msgid "Remote Controller" 186 | msgstr "" 187 | 188 | #: prefs.js:231 189 | msgid "Remote position" 190 | msgstr "" 191 | 192 | #: prefs.js:233 193 | msgid "Left" 194 | msgstr "" 195 | 196 | #: prefs.js:234 197 | msgid "Center (left side)" 198 | msgstr "" 199 | 200 | #: prefs.js:235 201 | msgid "Center (right side)" 202 | msgstr "" 203 | 204 | #: prefs.js:236 205 | msgid "Right" 206 | msgstr "" 207 | 208 | #: prefs.js:241 209 | msgid "Seek backward/forward (seconds)" 210 | msgstr "" 211 | 212 | #: prefs.js:251 213 | msgid "Slideshow time per picture (seconds)" 214 | msgstr "" 215 | 216 | #: prefs.js:261 217 | msgid "Media control buttons size" 218 | msgstr "" 219 | 220 | #: prefs.js:271 221 | msgid "Slider icon size" 222 | msgstr "" 223 | 224 | #: prefs.js:281 225 | msgid "Unify sliders" 226 | msgstr "" 227 | 228 | #: prefs.js:289 229 | msgid "Show remote label" 230 | msgstr "" 231 | 232 | #: prefs.js:297 233 | msgid "Receiver name as label" 234 | msgstr "" 235 | 236 | #: prefs.js:347 237 | msgid "Chromecast Options" 238 | msgstr "" 239 | 240 | #: prefs.js:351 prefs.js:663 241 | msgid "Device selection" 242 | msgstr "" 243 | 244 | #: prefs.js:378 245 | msgid "Subtitles" 246 | msgstr "" 247 | 248 | #: prefs.js:382 249 | msgid "Font family" 250 | msgstr "" 251 | 252 | #: prefs.js:401 253 | msgid "Font style" 254 | msgstr "" 255 | 256 | #. TRANSLATORS: One of "Speed" setting value 257 | #: prefs.js:403 node_scripts/messages.js:22 258 | msgid "Normal" 259 | msgstr "" 260 | 261 | #: prefs.js:404 262 | msgid "Bold" 263 | msgstr "" 264 | 265 | #: prefs.js:405 266 | msgid "Italic" 267 | msgstr "" 268 | 269 | #: prefs.js:406 270 | msgid "Bold italic" 271 | msgstr "" 272 | 273 | #: prefs.js:416 274 | msgid "Scale factor" 275 | msgstr "" 276 | 277 | #: prefs.js:430 278 | msgid "Font color" 279 | msgstr "" 280 | 281 | #: prefs.js:442 282 | msgid "Font outline" 283 | msgstr "" 284 | 285 | #: prefs.js:472 286 | msgid "Background color" 287 | msgstr "" 288 | 289 | #: prefs.js:540 290 | msgid "Encoder" 291 | msgstr "" 292 | 293 | #: prefs.js:545 294 | msgid "Media Encoding" 295 | msgstr "" 296 | 297 | #: prefs.js:549 298 | msgid "Hardware acceleration" 299 | msgstr "" 300 | 301 | #: prefs.js:551 prefs.js:1141 302 | msgid "None" 303 | msgstr "" 304 | 305 | #: prefs.js:558 306 | msgid "Bitrate (Mbps)" 307 | msgstr "" 308 | 309 | #: prefs.js:568 310 | msgid "Burn subtitles when transcoding video" 311 | msgstr "" 312 | 313 | #. TRANSLATORS: "Players" as video players 314 | #: prefs.js:583 315 | msgid "Extractor" 316 | msgstr "" 317 | 318 | #: prefs.js:588 319 | msgid "Subtitles Extraction" 320 | msgstr "" 321 | 322 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 323 | #: prefs.js:593 324 | msgid "Add vttextract executable" 325 | msgstr "" 326 | 327 | #: prefs.js:602 328 | msgid "Reuse extracted subtitles" 329 | msgstr "" 330 | 331 | #. TRANSLATORS: Destination folder to save subtitles 332 | #: prefs.js:611 333 | msgid "Save folder" 334 | msgstr "" 335 | 336 | #: prefs.js:613 337 | msgid "Select folder" 338 | msgstr "" 339 | 340 | #: prefs.js:654 341 | msgid "Misc" 342 | msgstr "" 343 | 344 | #: prefs.js:676 345 | msgid "Web Player" 346 | msgstr "" 347 | 348 | #: prefs.js:680 349 | msgid "Subtitles scale factor" 350 | msgstr "" 351 | 352 | #. TRANSLATORS: The rest of extension settings 353 | #: prefs.js:691 354 | msgid "Miscellaneous" 355 | msgstr "" 356 | 357 | #: prefs.js:695 358 | msgid "Music visualizer" 359 | msgstr "" 360 | 361 | #: prefs.js:703 362 | msgid "Nautilus/Nemo integration" 363 | msgstr "" 364 | 365 | #: prefs.js:809 366 | msgid "Install npm modules" 367 | msgstr "" 368 | 369 | #: prefs.js:833 370 | msgid "Installing..." 371 | msgstr "" 372 | 373 | #: prefs.js:887 374 | msgid "version:" 375 | msgstr "" 376 | 377 | #: prefs.js:894 378 | msgid "Developed by" 379 | msgstr "" 380 | 381 | #: prefs.js:903 382 | msgid "Extension Homepage" 383 | msgstr "" 384 | 385 | #: prefs.js:910 386 | msgid "Playercast Homepage" 387 | msgstr "" 388 | 389 | #: prefs.js:917 390 | msgid "Donate" 391 | msgstr "" 392 | 393 | #: prefs.js:931 394 | msgid "Main" 395 | msgstr "" 396 | 397 | #: prefs.js:935 398 | msgid "Remote" 399 | msgstr "" 400 | 401 | #. TRANSLATORS: Other extension settings 402 | #: prefs.js:944 403 | msgid "Other" 404 | msgstr "" 405 | 406 | #: prefs.js:957 407 | msgid "Add-ons" 408 | msgstr "" 409 | 410 | #: prefs.js:962 411 | msgid "Modules" 412 | msgstr "" 413 | 414 | #: prefs.js:966 415 | msgid "About" 416 | msgstr "" 417 | 418 | #: prefs.js:1074 419 | msgid "Manual IP Config" 420 | msgstr "" 421 | 422 | #: prefs.js:1131 423 | msgid "Auto" 424 | msgstr "" 425 | 426 | #: prefs.js:1132 427 | msgid "Name" 428 | msgstr "" 429 | 430 | #. TRANSLATORS: Text field temporary text 431 | #: prefs.js:1148 432 | msgid "Insert name" 433 | msgstr "" 434 | 435 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 436 | #: prefs.js:1263 437 | msgid "Scanning..." 438 | msgstr "" 439 | 440 | #: node_scripts/messages.js:4 441 | msgid "LOADING" 442 | msgstr "" 443 | 444 | #: node_scripts/messages.js:5 445 | msgid "No file selected" 446 | msgstr "" 447 | 448 | #: node_scripts/messages.js:6 449 | msgid "Receiver type is set to Chromecast" 450 | msgstr "" 451 | 452 | #: node_scripts/messages.js:7 453 | msgid "Receiver type is set to Playercast app" 454 | msgstr "" 455 | 456 | #: node_scripts/messages.js:8 457 | msgid "Streaming process is still active" 458 | msgstr "" 459 | 460 | #. TRANSLATORS: Do not remove HTML tags 461 | #: node_scripts/messages.js:10 462 | msgid "" 463 | "Too many connections!

Close all other tabs that are accessing this page in " 464 | "all browsers

" 465 | msgstr "" 466 | 467 | #. TRANSLATORS: This sentence will contain file path after end 468 | #: node_scripts/messages.js:12 469 | msgid "FFmpeg could not transcode file:" 470 | msgstr "" 471 | 472 | #: node_scripts/messages.js:13 473 | msgid "FFmpeg path is incorrect" 474 | msgstr "" 475 | 476 | #. TRANSLATORS: This sentence will contain file path after end 477 | #: node_scripts/messages.js:15 478 | msgid "FFprobe could not process file:" 479 | msgstr "" 480 | 481 | #: node_scripts/messages.js:16 482 | msgid "FFprobe path is incorrect" 483 | msgstr "" 484 | 485 | #. TRANSLATORS: This sentence will contain file path after end 486 | #: node_scripts/messages.js:18 487 | msgid "Could not analyze selected file:" 488 | msgstr "" 489 | 490 | #: node_scripts/messages.js:20 491 | msgid "Speed" 492 | msgstr "" 493 | 494 | #: node_scripts/messages.js:25 495 | msgid "Device not found" 496 | msgstr "" 497 | 498 | #: node_scripts/messages.js:26 499 | msgid "Failed to load media" 500 | msgstr "" 501 | 502 | #: node_scripts/messages.js:27 503 | msgid "Could not connect to device" 504 | msgstr "" 505 | 506 | #: node_scripts/messages.js:28 507 | msgid "Verify device IP in extension preferences" 508 | msgstr "" 509 | 510 | #. TRANSLATORS: This sentence will contain file path after end 511 | #: node_scripts/messages.js:30 512 | msgid "Could not play file:" 513 | msgstr "" 514 | 515 | #: node_scripts/messages.js:31 516 | msgid "Try again with transcoding enabled" 517 | msgstr "" 518 | -------------------------------------------------------------------------------- /po/cast-to-tv/en_GB.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: English, United Kingdom\n" 9 | "Language: en_GB\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: en-GB\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv/cast-to-tv.pot\n" 17 | 18 | #. TRANSLATORS: When "Cast Media" service is turned off 19 | #: widget.js:34 widget.js:111 20 | msgid "Cast Off" 21 | msgstr "" 22 | 23 | #: widget.js:39 file-chooser.js:80 24 | msgid "Video" 25 | msgstr "" 26 | 27 | #: widget.js:40 28 | msgid "Music" 29 | msgstr "" 30 | 31 | #: widget.js:41 32 | msgid "Picture" 33 | msgstr "" 34 | 35 | #: widget.js:42 widget.js:109 36 | msgid "Turn On" 37 | msgstr "" 38 | 39 | #: widget.js:43 40 | msgid "Cast Settings" 41 | msgstr "" 42 | 43 | #: widget.js:92 44 | msgid "Turn Off" 45 | msgstr "" 46 | 47 | #: widget.js:93 48 | msgid "Cast Media" 49 | msgstr "" 50 | 51 | #: widget.js:341 52 | msgid "Browser" 53 | msgstr "" 54 | 55 | #: playlist.js:344 56 | msgid "Playlist" 57 | msgstr "" 58 | 59 | #. TRANSLATORS: Button text when selected SINGLE file 60 | #: file-chooser.js:18 61 | msgid "Cast Selected File" 62 | msgstr "" 63 | 64 | #. TRANSLATORS: Button text when selected MULTIPLE files 65 | #: file-chooser.js:20 66 | msgid "Cast Selected Files" 67 | msgstr "" 68 | 69 | #: file-chooser.js:21 70 | msgid "Add to Playlist" 71 | msgstr "" 72 | 73 | #: file-chooser.js:77 74 | msgid "Transcode" 75 | msgstr "" 76 | 77 | #: file-chooser.js:81 78 | msgid "Audio" 79 | msgstr "" 80 | 81 | #: file-chooser.js:82 82 | msgid "Video + Audio" 83 | msgstr "" 84 | 85 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 86 | msgid "Automatic" 87 | msgstr "" 88 | 89 | #: file-chooser.js:270 90 | msgid "Cancel" 91 | msgstr "" 92 | 93 | #: file-chooser.js:279 94 | msgid "Add Subtitles" 95 | msgstr "" 96 | 97 | #: file-chooser.js:281 98 | msgid "Select Video" 99 | msgstr "" 100 | 101 | #: file-chooser.js:285 102 | msgid "Video Files" 103 | msgstr "" 104 | 105 | #: file-chooser.js:293 106 | msgid "Select Music" 107 | msgstr "" 108 | 109 | #: file-chooser.js:297 110 | msgid "Audio Files" 111 | msgstr "" 112 | 113 | #: file-chooser.js:305 114 | msgid "Select Picture" 115 | msgstr "" 116 | 117 | #: file-chooser.js:309 118 | msgid "Pictures" 119 | msgstr "" 120 | 121 | #: file-chooser.js:370 122 | msgid "Select Subtitles" 123 | msgstr "" 124 | 125 | #: file-chooser.js:374 126 | msgid "Subtitle Files" 127 | msgstr "" 128 | 129 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 130 | #: prefs.js:36 131 | msgid "is not installed" 132 | msgstr "" 133 | 134 | #: prefs.js:54 135 | msgid "Streaming in progress" 136 | msgstr "" 137 | 138 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 139 | #: prefs.js:64 140 | msgid "Stop media transfer before accessing extension settings" 141 | msgstr "" 142 | 143 | #: prefs.js:94 144 | msgid "Main Options" 145 | msgstr "" 146 | 147 | #: prefs.js:98 148 | msgid "Receiver type" 149 | msgstr "" 150 | 151 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 152 | #: prefs.js:102 prefs.js:659 153 | msgid "Playercast app" 154 | msgstr "" 155 | 156 | #. TRANSLATORS: Web browser or Media player app selection. 157 | #. This should be as short as possible e.g. "Browser | Player". 158 | #: prefs.js:105 159 | msgid "Web browser | Media player" 160 | msgstr "" 161 | 162 | #: prefs.js:110 163 | msgid "FFmpeg path" 164 | msgstr "" 165 | 166 | #: prefs.js:117 167 | msgid "FFprobe path" 168 | msgstr "" 169 | 170 | #: prefs.js:124 171 | msgid "Listening port" 172 | msgstr "" 173 | 174 | #: prefs.js:134 175 | msgid "Internal communication port" 176 | msgstr "" 177 | 178 | #: prefs.js:203 179 | msgid "Access web player from devices on local network" 180 | msgstr "" 181 | 182 | #: prefs.js:227 183 | msgid "Remote Controller" 184 | msgstr "" 185 | 186 | #: prefs.js:231 187 | msgid "Remote position" 188 | msgstr "" 189 | 190 | #: prefs.js:233 191 | msgid "Left" 192 | msgstr "" 193 | 194 | #: prefs.js:234 195 | msgid "Center (left side)" 196 | msgstr "" 197 | 198 | #: prefs.js:235 199 | msgid "Center (right side)" 200 | msgstr "" 201 | 202 | #: prefs.js:236 203 | msgid "Right" 204 | msgstr "" 205 | 206 | #: prefs.js:241 207 | msgid "Seek backward/forward (seconds)" 208 | msgstr "" 209 | 210 | #: prefs.js:251 211 | msgid "Slideshow time per picture (seconds)" 212 | msgstr "" 213 | 214 | #: prefs.js:261 215 | msgid "Media control buttons size" 216 | msgstr "" 217 | 218 | #: prefs.js:271 219 | msgid "Slider icon size" 220 | msgstr "" 221 | 222 | #: prefs.js:281 223 | msgid "Unify sliders" 224 | msgstr "" 225 | 226 | #: prefs.js:289 227 | msgid "Show remote label" 228 | msgstr "" 229 | 230 | #: prefs.js:297 231 | msgid "Receiver name as label" 232 | msgstr "" 233 | 234 | #: prefs.js:347 235 | msgid "Chromecast Options" 236 | msgstr "" 237 | 238 | #: prefs.js:351 prefs.js:663 239 | msgid "Device selection" 240 | msgstr "" 241 | 242 | #: prefs.js:378 243 | msgid "Subtitles" 244 | msgstr "" 245 | 246 | #: prefs.js:382 247 | msgid "Font family" 248 | msgstr "" 249 | 250 | #: prefs.js:401 251 | msgid "Font style" 252 | msgstr "" 253 | 254 | #. TRANSLATORS: One of "Speed" setting value 255 | #: prefs.js:403 node_scripts/messages.js:22 256 | msgid "Normal" 257 | msgstr "" 258 | 259 | #: prefs.js:404 260 | msgid "Bold" 261 | msgstr "" 262 | 263 | #: prefs.js:405 264 | msgid "Italic" 265 | msgstr "" 266 | 267 | #: prefs.js:406 268 | msgid "Bold italic" 269 | msgstr "" 270 | 271 | #: prefs.js:416 272 | msgid "Scale factor" 273 | msgstr "" 274 | 275 | #: prefs.js:430 276 | msgid "Font color" 277 | msgstr "Font colour" 278 | 279 | #: prefs.js:442 280 | msgid "Font outline" 281 | msgstr "" 282 | 283 | #: prefs.js:472 284 | msgid "Background color" 285 | msgstr "Background colour" 286 | 287 | #: prefs.js:540 288 | msgid "Encoder" 289 | msgstr "" 290 | 291 | #: prefs.js:545 292 | msgid "Media Encoding" 293 | msgstr "" 294 | 295 | #: prefs.js:549 296 | msgid "Hardware acceleration" 297 | msgstr "" 298 | 299 | #: prefs.js:551 prefs.js:1141 300 | msgid "None" 301 | msgstr "" 302 | 303 | #: prefs.js:558 304 | msgid "Bitrate (Mbps)" 305 | msgstr "" 306 | 307 | #: prefs.js:568 308 | msgid "Burn subtitles when transcoding video" 309 | msgstr "" 310 | 311 | #. TRANSLATORS: "Players" as video players 312 | #: prefs.js:583 313 | msgid "Extractor" 314 | msgstr "" 315 | 316 | #: prefs.js:588 317 | msgid "Subtitles Extraction" 318 | msgstr "" 319 | 320 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 321 | #: prefs.js:593 322 | msgid "Add vttextract executable" 323 | msgstr "" 324 | 325 | #: prefs.js:602 326 | msgid "Reuse extracted subtitles" 327 | msgstr "" 328 | 329 | #. TRANSLATORS: Destination folder to save subtitles 330 | #: prefs.js:611 331 | msgid "Save folder" 332 | msgstr "" 333 | 334 | #: prefs.js:613 335 | msgid "Select folder" 336 | msgstr "" 337 | 338 | #: prefs.js:654 339 | msgid "Misc" 340 | msgstr "" 341 | 342 | #: prefs.js:676 343 | msgid "Web Player" 344 | msgstr "" 345 | 346 | #: prefs.js:680 347 | msgid "Subtitles scale factor" 348 | msgstr "" 349 | 350 | #. TRANSLATORS: The rest of extension settings 351 | #: prefs.js:691 352 | msgid "Miscellaneous" 353 | msgstr "" 354 | 355 | #: prefs.js:695 356 | msgid "Music visualizer" 357 | msgstr "" 358 | 359 | #: prefs.js:703 360 | msgid "Nautilus/Nemo integration" 361 | msgstr "" 362 | 363 | #: prefs.js:809 364 | msgid "Install npm modules" 365 | msgstr "" 366 | 367 | #: prefs.js:833 368 | msgid "Installing..." 369 | msgstr "" 370 | 371 | #: prefs.js:887 372 | msgid "version:" 373 | msgstr "" 374 | 375 | #: prefs.js:894 376 | msgid "Developed by" 377 | msgstr "" 378 | 379 | #: prefs.js:903 380 | msgid "Extension Homepage" 381 | msgstr "" 382 | 383 | #: prefs.js:910 384 | msgid "Playercast Homepage" 385 | msgstr "" 386 | 387 | #: prefs.js:917 388 | msgid "Donate" 389 | msgstr "" 390 | 391 | #: prefs.js:931 392 | msgid "Main" 393 | msgstr "" 394 | 395 | #: prefs.js:935 396 | msgid "Remote" 397 | msgstr "" 398 | 399 | #. TRANSLATORS: Other extension settings 400 | #: prefs.js:944 401 | msgid "Other" 402 | msgstr "" 403 | 404 | #: prefs.js:957 405 | msgid "Add-ons" 406 | msgstr "" 407 | 408 | #: prefs.js:962 409 | msgid "Modules" 410 | msgstr "" 411 | 412 | #: prefs.js:966 413 | msgid "About" 414 | msgstr "" 415 | 416 | #: prefs.js:1074 417 | msgid "Manual IP Config" 418 | msgstr "" 419 | 420 | #: prefs.js:1131 421 | msgid "Auto" 422 | msgstr "" 423 | 424 | #: prefs.js:1132 425 | msgid "Name" 426 | msgstr "" 427 | 428 | #. TRANSLATORS: Text field temporary text 429 | #: prefs.js:1148 430 | msgid "Insert name" 431 | msgstr "" 432 | 433 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 434 | #: prefs.js:1263 435 | msgid "Scanning..." 436 | msgstr "" 437 | 438 | #: node_scripts/messages.js:4 439 | msgid "LOADING" 440 | msgstr "" 441 | 442 | #: node_scripts/messages.js:5 443 | msgid "No file selected" 444 | msgstr "" 445 | 446 | #: node_scripts/messages.js:6 447 | msgid "Receiver type is set to Chromecast" 448 | msgstr "" 449 | 450 | #: node_scripts/messages.js:7 451 | msgid "Receiver type is set to Playercast app" 452 | msgstr "" 453 | 454 | #: node_scripts/messages.js:8 455 | msgid "Streaming process is still active" 456 | msgstr "" 457 | 458 | #. TRANSLATORS: Do not remove HTML tags 459 | #: node_scripts/messages.js:10 460 | msgid "Too many connections!

Close all other tabs that are accessing this page in all browsers

" 461 | msgstr "" 462 | 463 | #. TRANSLATORS: This sentence will contain file path after end 464 | #: node_scripts/messages.js:12 465 | msgid "FFmpeg could not transcode file:" 466 | msgstr "" 467 | 468 | #: node_scripts/messages.js:13 469 | msgid "FFmpeg path is incorrect" 470 | msgstr "" 471 | 472 | #. TRANSLATORS: This sentence will contain file path after end 473 | #: node_scripts/messages.js:15 474 | msgid "FFprobe could not process file:" 475 | msgstr "" 476 | 477 | #: node_scripts/messages.js:16 478 | msgid "FFprobe path is incorrect" 479 | msgstr "" 480 | 481 | #. TRANSLATORS: This sentence will contain file path after end 482 | #: node_scripts/messages.js:18 483 | msgid "Could not analyze selected file:" 484 | msgstr "Could not analyse selected file:" 485 | 486 | #: node_scripts/messages.js:20 487 | msgid "Speed" 488 | msgstr "" 489 | 490 | #: node_scripts/messages.js:25 491 | msgid "Device not found" 492 | msgstr "" 493 | 494 | #: node_scripts/messages.js:26 495 | msgid "Failed to load media" 496 | msgstr "" 497 | 498 | #: node_scripts/messages.js:27 499 | msgid "Could not connect to device" 500 | msgstr "" 501 | 502 | #: node_scripts/messages.js:28 503 | msgid "Verify device IP in extension preferences" 504 | msgstr "" 505 | 506 | #. TRANSLATORS: This sentence will contain file path after end 507 | #: node_scripts/messages.js:30 508 | msgid "Could not play file:" 509 | msgstr "" 510 | 511 | #: node_scripts/messages.js:31 512 | msgid "Try again with transcoding enabled" 513 | msgstr "" 514 | 515 | -------------------------------------------------------------------------------- /po/cast-to-tv/fo.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Faroese\n" 9 | "Language: fo_FO\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: fo\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv/cast-to-tv.pot\n" 17 | 18 | #. TRANSLATORS: When "Cast Media" service is turned off 19 | #: widget.js:34 widget.js:111 20 | msgid "Cast Off" 21 | msgstr "" 22 | 23 | #: widget.js:39 file-chooser.js:80 24 | msgid "Video" 25 | msgstr "Sjónband" 26 | 27 | #: widget.js:40 28 | msgid "Music" 29 | msgstr "Tónleik" 30 | 31 | #: widget.js:41 32 | msgid "Picture" 33 | msgstr "Mynd" 34 | 35 | #: widget.js:42 widget.js:109 36 | msgid "Turn On" 37 | msgstr "" 38 | 39 | #: widget.js:43 40 | msgid "Cast Settings" 41 | msgstr "Varpar Setingar" 42 | 43 | #: widget.js:92 44 | msgid "Turn Off" 45 | msgstr "" 46 | 47 | #: widget.js:93 48 | msgid "Cast Media" 49 | msgstr "Varpa Media" 50 | 51 | #: widget.js:341 52 | msgid "Browser" 53 | msgstr "" 54 | 55 | #: playlist.js:344 56 | msgid "Playlist" 57 | msgstr "" 58 | 59 | #. TRANSLATORS: Button text when selected SINGLE file 60 | #: file-chooser.js:18 61 | msgid "Cast Selected File" 62 | msgstr "Varpa valdi fíluna" 63 | 64 | #. TRANSLATORS: Button text when selected MULTIPLE files 65 | #: file-chooser.js:20 66 | msgid "Cast Selected Files" 67 | msgstr "Varpa valdu fílurnar" 68 | 69 | #: file-chooser.js:21 70 | msgid "Add to Playlist" 71 | msgstr "" 72 | 73 | #: file-chooser.js:77 74 | msgid "Transcode" 75 | msgstr "" 76 | 77 | #: file-chooser.js:81 78 | msgid "Audio" 79 | msgstr "" 80 | 81 | #: file-chooser.js:82 82 | msgid "Video + Audio" 83 | msgstr "" 84 | 85 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 86 | msgid "Automatic" 87 | msgstr "Sjálvvirkið" 88 | 89 | #: file-chooser.js:270 90 | msgid "Cancel" 91 | msgstr "Avlýs" 92 | 93 | #: file-chooser.js:279 94 | msgid "Add Subtitles" 95 | msgstr "Legg undirtekstir aftrat" 96 | 97 | #: file-chooser.js:281 98 | msgid "Select Video" 99 | msgstr "Vel Sjónband" 100 | 101 | #: file-chooser.js:285 102 | msgid "Video Files" 103 | msgstr "Sjónbond" 104 | 105 | #: file-chooser.js:293 106 | msgid "Select Music" 107 | msgstr "Vel Tónleik" 108 | 109 | #: file-chooser.js:297 110 | msgid "Audio Files" 111 | msgstr "Tónleikar Fílur" 112 | 113 | #: file-chooser.js:305 114 | msgid "Select Picture" 115 | msgstr "Vel Mynd" 116 | 117 | #: file-chooser.js:309 118 | msgid "Pictures" 119 | msgstr "Myndir" 120 | 121 | #: file-chooser.js:370 122 | msgid "Select Subtitles" 123 | msgstr "Vel Undirtekstir" 124 | 125 | #: file-chooser.js:374 126 | msgid "Subtitle Files" 127 | msgstr "Undirteksta fílur" 128 | 129 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 130 | #: prefs.js:36 131 | msgid "is not installed" 132 | msgstr "" 133 | 134 | #: prefs.js:54 135 | msgid "Streaming in progress" 136 | msgstr "" 137 | 138 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 139 | #: prefs.js:64 140 | msgid "Stop media transfer before accessing extension settings" 141 | msgstr "" 142 | 143 | #: prefs.js:94 144 | msgid "Main Options" 145 | msgstr "" 146 | 147 | #: prefs.js:98 148 | msgid "Receiver type" 149 | msgstr "Slag av móttakara" 150 | 151 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 152 | #: prefs.js:102 prefs.js:659 153 | msgid "Playercast app" 154 | msgstr "" 155 | 156 | #. TRANSLATORS: Web browser or Media player app selection. 157 | #. This should be as short as possible e.g. "Browser | Player". 158 | #: prefs.js:105 159 | msgid "Web browser | Media player" 160 | msgstr "" 161 | 162 | #: prefs.js:110 163 | msgid "FFmpeg path" 164 | msgstr "FFmpeg slóð" 165 | 166 | #: prefs.js:117 167 | msgid "FFprobe path" 168 | msgstr "FFprobe slóð" 169 | 170 | #: prefs.js:124 171 | msgid "Listening port" 172 | msgstr "Portur at lurta at" 173 | 174 | #: prefs.js:134 175 | msgid "Internal communication port" 176 | msgstr "" 177 | 178 | #: prefs.js:203 179 | msgid "Access web player from devices on local network" 180 | msgstr "" 181 | 182 | #: prefs.js:227 183 | msgid "Remote Controller" 184 | msgstr "" 185 | 186 | #: prefs.js:231 187 | msgid "Remote position" 188 | msgstr "Remote position" 189 | 190 | #: prefs.js:233 191 | msgid "Left" 192 | msgstr "Vinstra" 193 | 194 | #: prefs.js:234 195 | msgid "Center (left side)" 196 | msgstr "Mitt fyri (vinstru síði)" 197 | 198 | #: prefs.js:235 199 | msgid "Center (right side)" 200 | msgstr "Mitt fyri (høgru síði)" 201 | 202 | #: prefs.js:236 203 | msgid "Right" 204 | msgstr "Høgra" 205 | 206 | #: prefs.js:241 207 | msgid "Seek backward/forward (seconds)" 208 | msgstr "Spola aftur/fram (sekund)" 209 | 210 | #: prefs.js:251 211 | msgid "Slideshow time per picture (seconds)" 212 | msgstr "" 213 | 214 | #: prefs.js:261 215 | msgid "Media control buttons size" 216 | msgstr "" 217 | 218 | #: prefs.js:271 219 | msgid "Slider icon size" 220 | msgstr "" 221 | 222 | #: prefs.js:281 223 | msgid "Unify sliders" 224 | msgstr "" 225 | 226 | #: prefs.js:289 227 | msgid "Show remote label" 228 | msgstr "" 229 | 230 | #: prefs.js:297 231 | msgid "Receiver name as label" 232 | msgstr "" 233 | 234 | #: prefs.js:347 235 | msgid "Chromecast Options" 236 | msgstr "" 237 | 238 | #: prefs.js:351 prefs.js:663 239 | msgid "Device selection" 240 | msgstr "" 241 | 242 | #: prefs.js:378 243 | msgid "Subtitles" 244 | msgstr "" 245 | 246 | #: prefs.js:382 247 | msgid "Font family" 248 | msgstr "" 249 | 250 | #: prefs.js:401 251 | msgid "Font style" 252 | msgstr "" 253 | 254 | #. TRANSLATORS: One of "Speed" setting value 255 | #: prefs.js:403 node_scripts/messages.js:22 256 | msgid "Normal" 257 | msgstr "Vanlig" 258 | 259 | #: prefs.js:404 260 | msgid "Bold" 261 | msgstr "" 262 | 263 | #: prefs.js:405 264 | msgid "Italic" 265 | msgstr "" 266 | 267 | #: prefs.js:406 268 | msgid "Bold italic" 269 | msgstr "" 270 | 271 | #: prefs.js:416 272 | msgid "Scale factor" 273 | msgstr "" 274 | 275 | #: prefs.js:430 276 | msgid "Font color" 277 | msgstr "" 278 | 279 | #: prefs.js:442 280 | msgid "Font outline" 281 | msgstr "" 282 | 283 | #: prefs.js:472 284 | msgid "Background color" 285 | msgstr "" 286 | 287 | #: prefs.js:540 288 | msgid "Encoder" 289 | msgstr "" 290 | 291 | #: prefs.js:545 292 | msgid "Media Encoding" 293 | msgstr "Media umkoting" 294 | 295 | #: prefs.js:549 296 | msgid "Hardware acceleration" 297 | msgstr "Hardware acceleration" 298 | 299 | #: prefs.js:551 prefs.js:1141 300 | msgid "None" 301 | msgstr "Ongin" 302 | 303 | #: prefs.js:558 304 | msgid "Bitrate (Mbps)" 305 | msgstr "Bitferð (Mdps)" 306 | 307 | #: prefs.js:568 308 | msgid "Burn subtitles when transcoding video" 309 | msgstr "" 310 | 311 | #. TRANSLATORS: "Players" as video players 312 | #: prefs.js:583 313 | msgid "Extractor" 314 | msgstr "" 315 | 316 | #: prefs.js:588 317 | msgid "Subtitles Extraction" 318 | msgstr "" 319 | 320 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 321 | #: prefs.js:593 322 | msgid "Add vttextract executable" 323 | msgstr "" 324 | 325 | #: prefs.js:602 326 | msgid "Reuse extracted subtitles" 327 | msgstr "" 328 | 329 | #. TRANSLATORS: Destination folder to save subtitles 330 | #: prefs.js:611 331 | msgid "Save folder" 332 | msgstr "" 333 | 334 | #: prefs.js:613 335 | msgid "Select folder" 336 | msgstr "" 337 | 338 | #: prefs.js:654 339 | msgid "Misc" 340 | msgstr "" 341 | 342 | #: prefs.js:676 343 | msgid "Web Player" 344 | msgstr "" 345 | 346 | #: prefs.js:680 347 | msgid "Subtitles scale factor" 348 | msgstr "" 349 | 350 | #. TRANSLATORS: The rest of extension settings 351 | #: prefs.js:691 352 | msgid "Miscellaneous" 353 | msgstr "Annað" 354 | 355 | #: prefs.js:695 356 | msgid "Music visualizer" 357 | msgstr "Vís endurgeving av tónleiki visuelt" 358 | 359 | #: prefs.js:703 360 | msgid "Nautilus/Nemo integration" 361 | msgstr "" 362 | 363 | #: prefs.js:809 364 | msgid "Install npm modules" 365 | msgstr "" 366 | 367 | #: prefs.js:833 368 | msgid "Installing..." 369 | msgstr "" 370 | 371 | #: prefs.js:887 372 | msgid "version:" 373 | msgstr "" 374 | 375 | #: prefs.js:894 376 | msgid "Developed by" 377 | msgstr "" 378 | 379 | #: prefs.js:903 380 | msgid "Extension Homepage" 381 | msgstr "" 382 | 383 | #: prefs.js:910 384 | msgid "Playercast Homepage" 385 | msgstr "" 386 | 387 | #: prefs.js:917 388 | msgid "Donate" 389 | msgstr "" 390 | 391 | #: prefs.js:931 392 | msgid "Main" 393 | msgstr "" 394 | 395 | #: prefs.js:935 396 | msgid "Remote" 397 | msgstr "" 398 | 399 | #. TRANSLATORS: Other extension settings 400 | #: prefs.js:944 401 | msgid "Other" 402 | msgstr "" 403 | 404 | #: prefs.js:957 405 | msgid "Add-ons" 406 | msgstr "" 407 | 408 | #: prefs.js:962 409 | msgid "Modules" 410 | msgstr "" 411 | 412 | #: prefs.js:966 413 | msgid "About" 414 | msgstr "" 415 | 416 | #: prefs.js:1074 417 | msgid "Manual IP Config" 418 | msgstr "" 419 | 420 | #: prefs.js:1131 421 | msgid "Auto" 422 | msgstr "" 423 | 424 | #: prefs.js:1132 425 | msgid "Name" 426 | msgstr "" 427 | 428 | #. TRANSLATORS: Text field temporary text 429 | #: prefs.js:1148 430 | msgid "Insert name" 431 | msgstr "" 432 | 433 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 434 | #: prefs.js:1263 435 | msgid "Scanning..." 436 | msgstr "Leitar..." 437 | 438 | #: node_scripts/messages.js:4 439 | msgid "LOADING" 440 | msgstr "ARBEIÐUR" 441 | 442 | #: node_scripts/messages.js:5 443 | msgid "No file selected" 444 | msgstr "Eingin fíla vald" 445 | 446 | #: node_scripts/messages.js:6 447 | msgid "Receiver type is set to Chromecast" 448 | msgstr "Móttakara slag er stilla til Chromecast" 449 | 450 | #: node_scripts/messages.js:7 451 | msgid "Receiver type is set to Playercast app" 452 | msgstr "" 453 | 454 | #: node_scripts/messages.js:8 455 | msgid "Streaming process is still active" 456 | msgstr "Varping er enn í gongd" 457 | 458 | #. TRANSLATORS: Do not remove HTML tags 459 | #: node_scripts/messages.js:10 460 | msgid "Too many connections!

Close all other tabs that are accessing this page in all browsers

" 461 | msgstr "" 462 | 463 | #. TRANSLATORS: This sentence will contain file path after end 464 | #: node_scripts/messages.js:12 465 | msgid "FFmpeg could not transcode file:" 466 | msgstr "FFMpeg kláraði ikki at umkota hesa fíluna:" 467 | 468 | #: node_scripts/messages.js:13 469 | msgid "FFmpeg path is incorrect" 470 | msgstr "FFmpeg slógin er ikki røtt" 471 | 472 | #. TRANSLATORS: This sentence will contain file path after end 473 | #: node_scripts/messages.js:15 474 | msgid "FFprobe could not process file:" 475 | msgstr "FFprobe fekk ikki viðgjørt hesa fíluna:" 476 | 477 | #: node_scripts/messages.js:16 478 | msgid "FFprobe path is incorrect" 479 | msgstr "FFprobe slógin er ikki røtt" 480 | 481 | #. TRANSLATORS: This sentence will contain file path after end 482 | #: node_scripts/messages.js:18 483 | msgid "Could not analyze selected file:" 484 | msgstr "" 485 | 486 | #: node_scripts/messages.js:20 487 | msgid "Speed" 488 | msgstr "Ferð" 489 | 490 | #: node_scripts/messages.js:25 491 | msgid "Device not found" 492 | msgstr "Tól ikki funnið" 493 | 494 | #: node_scripts/messages.js:26 495 | msgid "Failed to load media" 496 | msgstr "Fekk ikki lisið media" 497 | 498 | #: node_scripts/messages.js:27 499 | msgid "Could not connect to device" 500 | msgstr "" 501 | 502 | #: node_scripts/messages.js:28 503 | msgid "Verify device IP in extension preferences" 504 | msgstr "" 505 | 506 | #. TRANSLATORS: This sentence will contain file path after end 507 | #: node_scripts/messages.js:30 508 | msgid "Could not play file:" 509 | msgstr "Fekk ikki spælt fíluna:" 510 | 511 | #: node_scripts/messages.js:31 512 | msgid "Try again with transcoding enabled" 513 | msgstr "Royn aftur ímeðan umkoting er koblað til" 514 | 515 | -------------------------------------------------------------------------------- /po/cast-to-tv/fr.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 6 | "PO-Revision-Date: 2020-06-25 14:05\n" 7 | "Last-Translator: \n" 8 | "Language-Team: French\n" 9 | "Language: fr_FR\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Project-ID: 356831\n" 16 | "X-Crowdin-Language: fr\n" 17 | "X-Crowdin-File: /master/po/cast-to-tv/cast-to-tv.pot\n" 18 | "X-Crowdin-File-ID: 24\n" 19 | 20 | #. TRANSLATORS: When "Cast Media" service is turned off 21 | #: widget.js:34 widget.js:111 22 | msgid "Cast Off" 23 | msgstr "" 24 | 25 | #: widget.js:39 file-chooser.js:80 26 | msgid "Video" 27 | msgstr "" 28 | 29 | #: widget.js:40 30 | msgid "Music" 31 | msgstr "" 32 | 33 | #: widget.js:41 34 | msgid "Picture" 35 | msgstr "" 36 | 37 | #: widget.js:42 widget.js:109 38 | msgid "Turn On" 39 | msgstr "" 40 | 41 | #: widget.js:43 42 | msgid "Cast Settings" 43 | msgstr "" 44 | 45 | #: widget.js:92 46 | msgid "Turn Off" 47 | msgstr "" 48 | 49 | #: widget.js:93 50 | msgid "Cast Media" 51 | msgstr "" 52 | 53 | #: widget.js:341 54 | msgid "Browser" 55 | msgstr "" 56 | 57 | #: playlist.js:344 58 | msgid "Playlist" 59 | msgstr "" 60 | 61 | #. TRANSLATORS: Button text when selected SINGLE file 62 | #: file-chooser.js:18 63 | msgid "Cast Selected File" 64 | msgstr "" 65 | 66 | #. TRANSLATORS: Button text when selected MULTIPLE files 67 | #: file-chooser.js:20 68 | msgid "Cast Selected Files" 69 | msgstr "" 70 | 71 | #: file-chooser.js:21 72 | msgid "Add to Playlist" 73 | msgstr "" 74 | 75 | #: file-chooser.js:77 76 | msgid "Transcode" 77 | msgstr "" 78 | 79 | #: file-chooser.js:81 80 | msgid "Audio" 81 | msgstr "" 82 | 83 | #: file-chooser.js:82 84 | msgid "Video + Audio" 85 | msgstr "" 86 | 87 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 88 | msgid "Automatic" 89 | msgstr "" 90 | 91 | #: file-chooser.js:270 92 | msgid "Cancel" 93 | msgstr "" 94 | 95 | #: file-chooser.js:279 96 | msgid "Add Subtitles" 97 | msgstr "" 98 | 99 | #: file-chooser.js:281 100 | msgid "Select Video" 101 | msgstr "" 102 | 103 | #: file-chooser.js:285 104 | msgid "Video Files" 105 | msgstr "" 106 | 107 | #: file-chooser.js:293 108 | msgid "Select Music" 109 | msgstr "" 110 | 111 | #: file-chooser.js:297 112 | msgid "Audio Files" 113 | msgstr "" 114 | 115 | #: file-chooser.js:305 116 | msgid "Select Picture" 117 | msgstr "" 118 | 119 | #: file-chooser.js:309 120 | msgid "Pictures" 121 | msgstr "" 122 | 123 | #: file-chooser.js:370 124 | msgid "Select Subtitles" 125 | msgstr "" 126 | 127 | #: file-chooser.js:374 128 | msgid "Subtitle Files" 129 | msgstr "" 130 | 131 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 132 | #: prefs.js:36 133 | msgid "is not installed" 134 | msgstr "" 135 | 136 | #: prefs.js:54 137 | msgid "Streaming in progress" 138 | msgstr "" 139 | 140 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 141 | #: prefs.js:64 142 | msgid "Stop media transfer before accessing extension settings" 143 | msgstr "" 144 | 145 | #: prefs.js:94 146 | msgid "Main Options" 147 | msgstr "" 148 | 149 | #: prefs.js:98 150 | msgid "Receiver type" 151 | msgstr "" 152 | 153 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 154 | #: prefs.js:102 prefs.js:659 155 | msgid "Playercast app" 156 | msgstr "" 157 | 158 | #. TRANSLATORS: Web browser or Media player app selection. 159 | #. This should be as short as possible e.g. "Browser | Player". 160 | #: prefs.js:105 161 | msgid "Web browser | Media player" 162 | msgstr "" 163 | 164 | #: prefs.js:110 165 | msgid "FFmpeg path" 166 | msgstr "" 167 | 168 | #: prefs.js:117 169 | msgid "FFprobe path" 170 | msgstr "" 171 | 172 | #: prefs.js:124 173 | msgid "Listening port" 174 | msgstr "" 175 | 176 | #: prefs.js:134 177 | msgid "Internal communication port" 178 | msgstr "" 179 | 180 | #: prefs.js:203 181 | msgid "Access web player from devices on local network" 182 | msgstr "" 183 | 184 | #: prefs.js:227 185 | msgid "Remote Controller" 186 | msgstr "" 187 | 188 | #: prefs.js:231 189 | msgid "Remote position" 190 | msgstr "" 191 | 192 | #: prefs.js:233 193 | msgid "Left" 194 | msgstr "" 195 | 196 | #: prefs.js:234 197 | msgid "Center (left side)" 198 | msgstr "" 199 | 200 | #: prefs.js:235 201 | msgid "Center (right side)" 202 | msgstr "" 203 | 204 | #: prefs.js:236 205 | msgid "Right" 206 | msgstr "" 207 | 208 | #: prefs.js:241 209 | msgid "Seek backward/forward (seconds)" 210 | msgstr "" 211 | 212 | #: prefs.js:251 213 | msgid "Slideshow time per picture (seconds)" 214 | msgstr "" 215 | 216 | #: prefs.js:261 217 | msgid "Media control buttons size" 218 | msgstr "" 219 | 220 | #: prefs.js:271 221 | msgid "Slider icon size" 222 | msgstr "" 223 | 224 | #: prefs.js:281 225 | msgid "Unify sliders" 226 | msgstr "" 227 | 228 | #: prefs.js:289 229 | msgid "Show remote label" 230 | msgstr "" 231 | 232 | #: prefs.js:297 233 | msgid "Receiver name as label" 234 | msgstr "" 235 | 236 | #: prefs.js:347 237 | msgid "Chromecast Options" 238 | msgstr "" 239 | 240 | #: prefs.js:351 prefs.js:663 241 | msgid "Device selection" 242 | msgstr "" 243 | 244 | #: prefs.js:378 245 | msgid "Subtitles" 246 | msgstr "" 247 | 248 | #: prefs.js:382 249 | msgid "Font family" 250 | msgstr "" 251 | 252 | #: prefs.js:401 253 | msgid "Font style" 254 | msgstr "" 255 | 256 | #. TRANSLATORS: One of "Speed" setting value 257 | #: prefs.js:403 node_scripts/messages.js:22 258 | msgid "Normal" 259 | msgstr "" 260 | 261 | #: prefs.js:404 262 | msgid "Bold" 263 | msgstr "" 264 | 265 | #: prefs.js:405 266 | msgid "Italic" 267 | msgstr "" 268 | 269 | #: prefs.js:406 270 | msgid "Bold italic" 271 | msgstr "" 272 | 273 | #: prefs.js:416 274 | msgid "Scale factor" 275 | msgstr "" 276 | 277 | #: prefs.js:430 278 | msgid "Font color" 279 | msgstr "" 280 | 281 | #: prefs.js:442 282 | msgid "Font outline" 283 | msgstr "" 284 | 285 | #: prefs.js:472 286 | msgid "Background color" 287 | msgstr "" 288 | 289 | #: prefs.js:540 290 | msgid "Encoder" 291 | msgstr "" 292 | 293 | #: prefs.js:545 294 | msgid "Media Encoding" 295 | msgstr "" 296 | 297 | #: prefs.js:549 298 | msgid "Hardware acceleration" 299 | msgstr "" 300 | 301 | #: prefs.js:551 prefs.js:1141 302 | msgid "None" 303 | msgstr "" 304 | 305 | #: prefs.js:558 306 | msgid "Bitrate (Mbps)" 307 | msgstr "" 308 | 309 | #: prefs.js:568 310 | msgid "Burn subtitles when transcoding video" 311 | msgstr "" 312 | 313 | #. TRANSLATORS: "Players" as video players 314 | #: prefs.js:583 315 | msgid "Extractor" 316 | msgstr "" 317 | 318 | #: prefs.js:588 319 | msgid "Subtitles Extraction" 320 | msgstr "" 321 | 322 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 323 | #: prefs.js:593 324 | msgid "Add vttextract executable" 325 | msgstr "" 326 | 327 | #: prefs.js:602 328 | msgid "Reuse extracted subtitles" 329 | msgstr "" 330 | 331 | #. TRANSLATORS: Destination folder to save subtitles 332 | #: prefs.js:611 333 | msgid "Save folder" 334 | msgstr "" 335 | 336 | #: prefs.js:613 337 | msgid "Select folder" 338 | msgstr "" 339 | 340 | #: prefs.js:654 341 | msgid "Misc" 342 | msgstr "" 343 | 344 | #: prefs.js:676 345 | msgid "Web Player" 346 | msgstr "" 347 | 348 | #: prefs.js:680 349 | msgid "Subtitles scale factor" 350 | msgstr "" 351 | 352 | #. TRANSLATORS: The rest of extension settings 353 | #: prefs.js:691 354 | msgid "Miscellaneous" 355 | msgstr "" 356 | 357 | #: prefs.js:695 358 | msgid "Music visualizer" 359 | msgstr "" 360 | 361 | #: prefs.js:703 362 | msgid "Nautilus/Nemo integration" 363 | msgstr "" 364 | 365 | #: prefs.js:809 366 | msgid "Install npm modules" 367 | msgstr "" 368 | 369 | #: prefs.js:833 370 | msgid "Installing..." 371 | msgstr "" 372 | 373 | #: prefs.js:887 374 | msgid "version:" 375 | msgstr "" 376 | 377 | #: prefs.js:894 378 | msgid "Developed by" 379 | msgstr "" 380 | 381 | #: prefs.js:903 382 | msgid "Extension Homepage" 383 | msgstr "" 384 | 385 | #: prefs.js:910 386 | msgid "Playercast Homepage" 387 | msgstr "" 388 | 389 | #: prefs.js:917 390 | msgid "Donate" 391 | msgstr "" 392 | 393 | #: prefs.js:931 394 | msgid "Main" 395 | msgstr "" 396 | 397 | #: prefs.js:935 398 | msgid "Remote" 399 | msgstr "" 400 | 401 | #. TRANSLATORS: Other extension settings 402 | #: prefs.js:944 403 | msgid "Other" 404 | msgstr "" 405 | 406 | #: prefs.js:957 407 | msgid "Add-ons" 408 | msgstr "" 409 | 410 | #: prefs.js:962 411 | msgid "Modules" 412 | msgstr "" 413 | 414 | #: prefs.js:966 415 | msgid "About" 416 | msgstr "" 417 | 418 | #: prefs.js:1074 419 | msgid "Manual IP Config" 420 | msgstr "" 421 | 422 | #: prefs.js:1131 423 | msgid "Auto" 424 | msgstr "" 425 | 426 | #: prefs.js:1132 427 | msgid "Name" 428 | msgstr "" 429 | 430 | #. TRANSLATORS: Text field temporary text 431 | #: prefs.js:1148 432 | msgid "Insert name" 433 | msgstr "" 434 | 435 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 436 | #: prefs.js:1263 437 | msgid "Scanning..." 438 | msgstr "" 439 | 440 | #: node_scripts/messages.js:4 441 | msgid "LOADING" 442 | msgstr "" 443 | 444 | #: node_scripts/messages.js:5 445 | msgid "No file selected" 446 | msgstr "" 447 | 448 | #: node_scripts/messages.js:6 449 | msgid "Receiver type is set to Chromecast" 450 | msgstr "" 451 | 452 | #: node_scripts/messages.js:7 453 | msgid "Receiver type is set to Playercast app" 454 | msgstr "" 455 | 456 | #: node_scripts/messages.js:8 457 | msgid "Streaming process is still active" 458 | msgstr "" 459 | 460 | #. TRANSLATORS: Do not remove HTML tags 461 | #: node_scripts/messages.js:10 462 | msgid "Too many connections!

Close all other tabs that are accessing this page in all browsers

" 463 | msgstr "" 464 | 465 | #. TRANSLATORS: This sentence will contain file path after end 466 | #: node_scripts/messages.js:12 467 | msgid "FFmpeg could not transcode file:" 468 | msgstr "" 469 | 470 | #: node_scripts/messages.js:13 471 | msgid "FFmpeg path is incorrect" 472 | msgstr "" 473 | 474 | #. TRANSLATORS: This sentence will contain file path after end 475 | #: node_scripts/messages.js:15 476 | msgid "FFprobe could not process file:" 477 | msgstr "" 478 | 479 | #: node_scripts/messages.js:16 480 | msgid "FFprobe path is incorrect" 481 | msgstr "" 482 | 483 | #. TRANSLATORS: This sentence will contain file path after end 484 | #: node_scripts/messages.js:18 485 | msgid "Could not analyze selected file:" 486 | msgstr "" 487 | 488 | #: node_scripts/messages.js:20 489 | msgid "Speed" 490 | msgstr "" 491 | 492 | #: node_scripts/messages.js:25 493 | msgid "Device not found" 494 | msgstr "" 495 | 496 | #: node_scripts/messages.js:26 497 | msgid "Failed to load media" 498 | msgstr "" 499 | 500 | #: node_scripts/messages.js:27 501 | msgid "Could not connect to device" 502 | msgstr "" 503 | 504 | #: node_scripts/messages.js:28 505 | msgid "Verify device IP in extension preferences" 506 | msgstr "" 507 | 508 | #. TRANSLATORS: This sentence will contain file path after end 509 | #: node_scripts/messages.js:30 510 | msgid "Could not play file:" 511 | msgstr "" 512 | 513 | #: node_scripts/messages.js:31 514 | msgid "Try again with transcoding enabled" 515 | msgstr "" 516 | 517 | -------------------------------------------------------------------------------- /po/cast-to-tv/nl.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 6 | "PO-Revision-Date: 2020-02-04 15:48\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Dutch\n" 9 | "Language: nl_NL\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: nl\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv/cast-to-tv.pot\n" 17 | 18 | #. TRANSLATORS: When "Cast Media" service is turned off 19 | #: widget.js:34 widget.js:111 20 | msgid "Cast Off" 21 | msgstr "Casten is uitgeschakeld" 22 | 23 | #: widget.js:39 file-chooser.js:80 24 | msgid "Video" 25 | msgstr "Video" 26 | 27 | #: widget.js:40 28 | msgid "Music" 29 | msgstr "Muziek" 30 | 31 | #: widget.js:41 32 | msgid "Picture" 33 | msgstr "Afbeelding" 34 | 35 | #: widget.js:42 widget.js:109 36 | msgid "Turn On" 37 | msgstr "Inschakelen" 38 | 39 | #: widget.js:43 40 | msgid "Cast Settings" 41 | msgstr "Cast-instellingen" 42 | 43 | #: widget.js:92 44 | msgid "Turn Off" 45 | msgstr "Uitschakelen" 46 | 47 | #: widget.js:93 48 | msgid "Cast Media" 49 | msgstr "Media casten" 50 | 51 | #: widget.js:341 52 | msgid "Browser" 53 | msgstr "" 54 | 55 | #: playlist.js:344 56 | msgid "Playlist" 57 | msgstr "" 58 | 59 | #. TRANSLATORS: Button text when selected SINGLE file 60 | #: file-chooser.js:18 61 | msgid "Cast Selected File" 62 | msgstr "Bestand casten" 63 | 64 | #. TRANSLATORS: Button text when selected MULTIPLE files 65 | #: file-chooser.js:20 66 | msgid "Cast Selected Files" 67 | msgstr "Bestanden casten" 68 | 69 | #: file-chooser.js:21 70 | msgid "Add to Playlist" 71 | msgstr "" 72 | 73 | #: file-chooser.js:77 74 | msgid "Transcode" 75 | msgstr "" 76 | 77 | #: file-chooser.js:81 78 | msgid "Audio" 79 | msgstr "" 80 | 81 | #: file-chooser.js:82 82 | msgid "Video + Audio" 83 | msgstr "" 84 | 85 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 86 | msgid "Automatic" 87 | msgstr "Automatisch" 88 | 89 | #: file-chooser.js:270 90 | msgid "Cancel" 91 | msgstr "Annuleren" 92 | 93 | #: file-chooser.js:279 94 | msgid "Add Subtitles" 95 | msgstr "Ondertiteling toevoegen" 96 | 97 | #: file-chooser.js:281 98 | msgid "Select Video" 99 | msgstr "Video selecteren" 100 | 101 | #: file-chooser.js:285 102 | msgid "Video Files" 103 | msgstr "Videobestanden" 104 | 105 | #: file-chooser.js:293 106 | msgid "Select Music" 107 | msgstr "Muziek selecteren" 108 | 109 | #: file-chooser.js:297 110 | msgid "Audio Files" 111 | msgstr "Audiobestanden" 112 | 113 | #: file-chooser.js:305 114 | msgid "Select Picture" 115 | msgstr "Afbeelding selecteren" 116 | 117 | #: file-chooser.js:309 118 | msgid "Pictures" 119 | msgstr "Afbeeldingen" 120 | 121 | #: file-chooser.js:370 122 | msgid "Select Subtitles" 123 | msgstr "Ondertiteling selecteren" 124 | 125 | #: file-chooser.js:374 126 | msgid "Subtitle Files" 127 | msgstr "Ondertitelingsbestanden" 128 | 129 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 130 | #: prefs.js:36 131 | msgid "is not installed" 132 | msgstr "is niet geïnstalleerd" 133 | 134 | #: prefs.js:54 135 | msgid "Streaming in progress" 136 | msgstr "Bezig met streamen" 137 | 138 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 139 | #: prefs.js:64 140 | msgid "Stop media transfer before accessing extension settings" 141 | msgstr "Stop de mediaoverdracht voordat je de uitbreidingsinstellingen opent" 142 | 143 | #: prefs.js:94 144 | msgid "Main Options" 145 | msgstr "Algemene instellingen" 146 | 147 | #: prefs.js:98 148 | msgid "Receiver type" 149 | msgstr "Soort ontvanger" 150 | 151 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 152 | #: prefs.js:102 prefs.js:659 153 | msgid "Playercast app" 154 | msgstr "Playercast-app" 155 | 156 | #. TRANSLATORS: Web browser or Media player app selection. 157 | #. This should be as short as possible e.g. "Browser | Player". 158 | #: prefs.js:105 159 | msgid "Web browser | Media player" 160 | msgstr "Webbrowser | Mediaspeler" 161 | 162 | #: prefs.js:110 163 | msgid "FFmpeg path" 164 | msgstr "FFmpeg-pad" 165 | 166 | #: prefs.js:117 167 | msgid "FFprobe path" 168 | msgstr "FFprobe-pad" 169 | 170 | #: prefs.js:124 171 | msgid "Listening port" 172 | msgstr "Luisteren op poort" 173 | 174 | #: prefs.js:134 175 | msgid "Internal communication port" 176 | msgstr "" 177 | 178 | #: prefs.js:203 179 | msgid "Access web player from devices on local network" 180 | msgstr "Toegang tot de webspeler vanaf apparaten op het lokale netwerk" 181 | 182 | #: prefs.js:227 183 | msgid "Remote Controller" 184 | msgstr "Afstandsbediening" 185 | 186 | #: prefs.js:231 187 | msgid "Remote position" 188 | msgstr "Positie" 189 | 190 | #: prefs.js:233 191 | msgid "Left" 192 | msgstr "Links" 193 | 194 | #: prefs.js:234 195 | msgid "Center (left side)" 196 | msgstr "Gecentreerd (linkerkant)" 197 | 198 | #: prefs.js:235 199 | msgid "Center (right side)" 200 | msgstr "Gecentreerd (rechterkant)" 201 | 202 | #: prefs.js:236 203 | msgid "Right" 204 | msgstr "Rechts" 205 | 206 | #: prefs.js:241 207 | msgid "Seek backward/forward (seconds)" 208 | msgstr "Terug-/Vooruitspoelen (in seconden)" 209 | 210 | #: prefs.js:251 211 | msgid "Slideshow time per picture (seconds)" 212 | msgstr "" 213 | 214 | #: prefs.js:261 215 | msgid "Media control buttons size" 216 | msgstr "" 217 | 218 | #: prefs.js:271 219 | msgid "Slider icon size" 220 | msgstr "" 221 | 222 | #: prefs.js:281 223 | msgid "Unify sliders" 224 | msgstr "Schuifregelaars gelijktrekken" 225 | 226 | #: prefs.js:289 227 | msgid "Show remote label" 228 | msgstr "Labels tonen" 229 | 230 | #: prefs.js:297 231 | msgid "Receiver name as label" 232 | msgstr "" 233 | 234 | #: prefs.js:347 235 | msgid "Chromecast Options" 236 | msgstr "Chromecast-instellingen" 237 | 238 | #: prefs.js:351 prefs.js:663 239 | msgid "Device selection" 240 | msgstr "Apparaatkeuze" 241 | 242 | #: prefs.js:378 243 | msgid "Subtitles" 244 | msgstr "Ondertiteling" 245 | 246 | #: prefs.js:382 247 | msgid "Font family" 248 | msgstr "Lettertype" 249 | 250 | #: prefs.js:401 251 | msgid "Font style" 252 | msgstr "Lettertypestijl" 253 | 254 | #. TRANSLATORS: One of "Speed" setting value 255 | #: prefs.js:403 node_scripts/messages.js:22 256 | msgid "Normal" 257 | msgstr "Normaal" 258 | 259 | #: prefs.js:404 260 | msgid "Bold" 261 | msgstr "Vetgedrukt" 262 | 263 | #: prefs.js:405 264 | msgid "Italic" 265 | msgstr "Cursief" 266 | 267 | #: prefs.js:406 268 | msgid "Bold italic" 269 | msgstr "Vetgedrukt cursief" 270 | 271 | #: prefs.js:416 272 | msgid "Scale factor" 273 | msgstr "Schaalfactor" 274 | 275 | #: prefs.js:430 276 | msgid "Font color" 277 | msgstr "Lettertypekleur" 278 | 279 | #: prefs.js:442 280 | msgid "Font outline" 281 | msgstr "Lettertype-omlijning" 282 | 283 | #: prefs.js:472 284 | msgid "Background color" 285 | msgstr "Achtergrondkleur" 286 | 287 | #: prefs.js:540 288 | msgid "Encoder" 289 | msgstr "" 290 | 291 | #: prefs.js:545 292 | msgid "Media Encoding" 293 | msgstr "Mediacodering" 294 | 295 | #: prefs.js:549 296 | msgid "Hardware acceleration" 297 | msgstr "Hardwareversnelling" 298 | 299 | #: prefs.js:551 prefs.js:1141 300 | msgid "None" 301 | msgstr "Geen" 302 | 303 | #: prefs.js:558 304 | msgid "Bitrate (Mbps)" 305 | msgstr "Bitsnelheid (Mbps)" 306 | 307 | #: prefs.js:568 308 | msgid "Burn subtitles when transcoding video" 309 | msgstr "" 310 | 311 | #. TRANSLATORS: "Players" as video players 312 | #: prefs.js:583 313 | msgid "Extractor" 314 | msgstr "" 315 | 316 | #: prefs.js:588 317 | msgid "Subtitles Extraction" 318 | msgstr "" 319 | 320 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 321 | #: prefs.js:593 322 | msgid "Add vttextract executable" 323 | msgstr "" 324 | 325 | #: prefs.js:602 326 | msgid "Reuse extracted subtitles" 327 | msgstr "" 328 | 329 | #. TRANSLATORS: Destination folder to save subtitles 330 | #: prefs.js:611 331 | msgid "Save folder" 332 | msgstr "" 333 | 334 | #: prefs.js:613 335 | msgid "Select folder" 336 | msgstr "" 337 | 338 | #: prefs.js:654 339 | msgid "Misc" 340 | msgstr "" 341 | 342 | #: prefs.js:676 343 | msgid "Web Player" 344 | msgstr "Webspeler" 345 | 346 | #: prefs.js:680 347 | msgid "Subtitles scale factor" 348 | msgstr "Schaal van ondertiteling" 349 | 350 | #. TRANSLATORS: The rest of extension settings 351 | #: prefs.js:691 352 | msgid "Miscellaneous" 353 | msgstr "Overig" 354 | 355 | #: prefs.js:695 356 | msgid "Music visualizer" 357 | msgstr "Muziekvisualisatie" 358 | 359 | #: prefs.js:703 360 | msgid "Nautilus/Nemo integration" 361 | msgstr "" 362 | 363 | #: prefs.js:809 364 | msgid "Install npm modules" 365 | msgstr "npm-modules installeren" 366 | 367 | #: prefs.js:833 368 | msgid "Installing..." 369 | msgstr "Bezig met installeren..." 370 | 371 | #: prefs.js:887 372 | msgid "version:" 373 | msgstr "versie:" 374 | 375 | #: prefs.js:894 376 | msgid "Developed by" 377 | msgstr "Ontwikkeld door" 378 | 379 | #: prefs.js:903 380 | msgid "Extension Homepage" 381 | msgstr "Website" 382 | 383 | #: prefs.js:910 384 | msgid "Playercast Homepage" 385 | msgstr "Playercast-website" 386 | 387 | #: prefs.js:917 388 | msgid "Donate" 389 | msgstr "Doneren" 390 | 391 | #: prefs.js:931 392 | msgid "Main" 393 | msgstr "Algemeen" 394 | 395 | #: prefs.js:935 396 | msgid "Remote" 397 | msgstr "Op afstand" 398 | 399 | #. TRANSLATORS: Other extension settings 400 | #: prefs.js:944 401 | msgid "Other" 402 | msgstr "Anders" 403 | 404 | #: prefs.js:957 405 | msgid "Add-ons" 406 | msgstr "Uitbreidingen" 407 | 408 | #: prefs.js:962 409 | msgid "Modules" 410 | msgstr "Modules" 411 | 412 | #: prefs.js:966 413 | msgid "About" 414 | msgstr "Over" 415 | 416 | #: prefs.js:1074 417 | msgid "Manual IP Config" 418 | msgstr "" 419 | 420 | #: prefs.js:1131 421 | msgid "Auto" 422 | msgstr "" 423 | 424 | #: prefs.js:1132 425 | msgid "Name" 426 | msgstr "" 427 | 428 | #. TRANSLATORS: Text field temporary text 429 | #: prefs.js:1148 430 | msgid "Insert name" 431 | msgstr "" 432 | 433 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 434 | #: prefs.js:1263 435 | msgid "Scanning..." 436 | msgstr "Bezig met zoeken..." 437 | 438 | #: node_scripts/messages.js:4 439 | msgid "LOADING" 440 | msgstr "BEZIG MET LADEN" 441 | 442 | #: node_scripts/messages.js:5 443 | msgid "No file selected" 444 | msgstr "Geen bestand gekozen" 445 | 446 | #: node_scripts/messages.js:6 447 | msgid "Receiver type is set to Chromecast" 448 | msgstr "Het soort ontvanger is ingesteld op Chromecast" 449 | 450 | #: node_scripts/messages.js:7 451 | msgid "Receiver type is set to Playercast app" 452 | msgstr "Het soort ontvanger is ingesteld op Playercast" 453 | 454 | #: node_scripts/messages.js:8 455 | msgid "Streaming process is still active" 456 | msgstr "Het streamproces loopt nog" 457 | 458 | #. TRANSLATORS: Do not remove HTML tags 459 | #: node_scripts/messages.js:10 460 | msgid "Too many connections!

Close all other tabs that are accessing this page in all browsers

" 461 | msgstr "Te veel verbindingen!

Sluit alle andere tabbladen die toegang hebben tot deze pagina in alle browsers

" 462 | 463 | #. TRANSLATORS: This sentence will contain file path after end 464 | #: node_scripts/messages.js:12 465 | msgid "FFmpeg could not transcode file:" 466 | msgstr "FFmpeg kan het bestand niet converteren:" 467 | 468 | #: node_scripts/messages.js:13 469 | msgid "FFmpeg path is incorrect" 470 | msgstr "Onjuist FFmpeg-pad" 471 | 472 | #. TRANSLATORS: This sentence will contain file path after end 473 | #: node_scripts/messages.js:15 474 | msgid "FFprobe could not process file:" 475 | msgstr "FFprobe kan het bestand niet verwerken:" 476 | 477 | #: node_scripts/messages.js:16 478 | msgid "FFprobe path is incorrect" 479 | msgstr "Onjuist FFprobe-pad" 480 | 481 | #. TRANSLATORS: This sentence will contain file path after end 482 | #: node_scripts/messages.js:18 483 | msgid "Could not analyze selected file:" 484 | msgstr "" 485 | 486 | #: node_scripts/messages.js:20 487 | msgid "Speed" 488 | msgstr "Snelheid" 489 | 490 | #: node_scripts/messages.js:25 491 | msgid "Device not found" 492 | msgstr "Apparaat niet gevonden" 493 | 494 | #: node_scripts/messages.js:26 495 | msgid "Failed to load media" 496 | msgstr "Kan media niet laden" 497 | 498 | #: node_scripts/messages.js:27 499 | msgid "Could not connect to device" 500 | msgstr "" 501 | 502 | #: node_scripts/messages.js:28 503 | msgid "Verify device IP in extension preferences" 504 | msgstr "" 505 | 506 | #. TRANSLATORS: This sentence will contain file path after end 507 | #: node_scripts/messages.js:30 508 | msgid "Could not play file:" 509 | msgstr "Kan bestand niet afspelen:" 510 | 511 | #: node_scripts/messages.js:31 512 | msgid "Try again with transcoding enabled" 513 | msgstr "Probeer het opnieuw met converteren ingeschakeld" 514 | 515 | -------------------------------------------------------------------------------- /po/cast-to-tv/ro.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: cast-to-tv\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2020-02-04 16:20+0100\n" 6 | "PO-Revision-Date: 2020-02-21 10:05\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: Romanian\n" 9 | "Language: ro_RO\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" 14 | "X-Crowdin-Project: cast-to-tv\n" 15 | "X-Crowdin-Language: ro\n" 16 | "X-Crowdin-File: /master/po/cast-to-tv/cast-to-tv.pot\n" 17 | 18 | #. TRANSLATORS: When "Cast Media" service is turned off 19 | #: widget.js:34 widget.js:111 20 | msgid "Cast Off" 21 | msgstr "" 22 | 23 | #: widget.js:39 file-chooser.js:80 24 | msgid "Video" 25 | msgstr "" 26 | 27 | #: widget.js:40 28 | msgid "Music" 29 | msgstr "" 30 | 31 | #: widget.js:41 32 | msgid "Picture" 33 | msgstr "" 34 | 35 | #: widget.js:42 widget.js:109 36 | msgid "Turn On" 37 | msgstr "" 38 | 39 | #: widget.js:43 40 | msgid "Cast Settings" 41 | msgstr "" 42 | 43 | #: widget.js:92 44 | msgid "Turn Off" 45 | msgstr "" 46 | 47 | #: widget.js:93 48 | msgid "Cast Media" 49 | msgstr "" 50 | 51 | #: widget.js:341 52 | msgid "Browser" 53 | msgstr "" 54 | 55 | #: playlist.js:344 56 | msgid "Playlist" 57 | msgstr "" 58 | 59 | #. TRANSLATORS: Button text when selected SINGLE file 60 | #: file-chooser.js:18 61 | msgid "Cast Selected File" 62 | msgstr "" 63 | 64 | #. TRANSLATORS: Button text when selected MULTIPLE files 65 | #: file-chooser.js:20 66 | msgid "Cast Selected Files" 67 | msgstr "" 68 | 69 | #: file-chooser.js:21 70 | msgid "Add to Playlist" 71 | msgstr "" 72 | 73 | #: file-chooser.js:77 74 | msgid "Transcode" 75 | msgstr "" 76 | 77 | #: file-chooser.js:81 78 | msgid "Audio" 79 | msgstr "" 80 | 81 | #: file-chooser.js:82 82 | msgid "Video + Audio" 83 | msgstr "" 84 | 85 | #: file-chooser.js:142 file-chooser.js:175 prefs.js:1283 86 | msgid "Automatic" 87 | msgstr "" 88 | 89 | #: file-chooser.js:270 90 | msgid "Cancel" 91 | msgstr "" 92 | 93 | #: file-chooser.js:279 94 | msgid "Add Subtitles" 95 | msgstr "" 96 | 97 | #: file-chooser.js:281 98 | msgid "Select Video" 99 | msgstr "" 100 | 101 | #: file-chooser.js:285 102 | msgid "Video Files" 103 | msgstr "" 104 | 105 | #: file-chooser.js:293 106 | msgid "Select Music" 107 | msgstr "" 108 | 109 | #: file-chooser.js:297 110 | msgid "Audio Files" 111 | msgstr "" 112 | 113 | #: file-chooser.js:305 114 | msgid "Select Picture" 115 | msgstr "" 116 | 117 | #: file-chooser.js:309 118 | msgid "Pictures" 119 | msgstr "" 120 | 121 | #: file-chooser.js:370 122 | msgid "Select Subtitles" 123 | msgstr "" 124 | 125 | #: file-chooser.js:374 126 | msgid "Subtitle Files" 127 | msgstr "" 128 | 129 | #. TRANSLATORS: Will contain dependency name at the beginning (e.g. Node.js is not installed) 130 | #: prefs.js:36 131 | msgid "is not installed" 132 | msgstr "" 133 | 134 | #: prefs.js:54 135 | msgid "Streaming in progress" 136 | msgstr "" 137 | 138 | #. TRANSLATORS: Keep line this short (otherwise extension prefs will strech) 139 | #: prefs.js:64 140 | msgid "Stop media transfer before accessing extension settings" 141 | msgstr "" 142 | 143 | #: prefs.js:94 144 | msgid "Main Options" 145 | msgstr "" 146 | 147 | #: prefs.js:98 148 | msgid "Receiver type" 149 | msgstr "" 150 | 151 | #. TRANSLATORS: "Playercast" is a name of an app, so do not change it 152 | #: prefs.js:102 prefs.js:659 153 | msgid "Playercast app" 154 | msgstr "" 155 | 156 | #. TRANSLATORS: Web browser or Media player app selection. 157 | #. This should be as short as possible e.g. "Browser | Player". 158 | #: prefs.js:105 159 | msgid "Web browser | Media player" 160 | msgstr "" 161 | 162 | #: prefs.js:110 163 | msgid "FFmpeg path" 164 | msgstr "" 165 | 166 | #: prefs.js:117 167 | msgid "FFprobe path" 168 | msgstr "" 169 | 170 | #: prefs.js:124 171 | msgid "Listening port" 172 | msgstr "" 173 | 174 | #: prefs.js:134 175 | msgid "Internal communication port" 176 | msgstr "" 177 | 178 | #: prefs.js:203 179 | msgid "Access web player from devices on local network" 180 | msgstr "" 181 | 182 | #: prefs.js:227 183 | msgid "Remote Controller" 184 | msgstr "" 185 | 186 | #: prefs.js:231 187 | msgid "Remote position" 188 | msgstr "" 189 | 190 | #: prefs.js:233 191 | msgid "Left" 192 | msgstr "" 193 | 194 | #: prefs.js:234 195 | msgid "Center (left side)" 196 | msgstr "" 197 | 198 | #: prefs.js:235 199 | msgid "Center (right side)" 200 | msgstr "" 201 | 202 | #: prefs.js:236 203 | msgid "Right" 204 | msgstr "" 205 | 206 | #: prefs.js:241 207 | msgid "Seek backward/forward (seconds)" 208 | msgstr "" 209 | 210 | #: prefs.js:251 211 | msgid "Slideshow time per picture (seconds)" 212 | msgstr "" 213 | 214 | #: prefs.js:261 215 | msgid "Media control buttons size" 216 | msgstr "" 217 | 218 | #: prefs.js:271 219 | msgid "Slider icon size" 220 | msgstr "" 221 | 222 | #: prefs.js:281 223 | msgid "Unify sliders" 224 | msgstr "" 225 | 226 | #: prefs.js:289 227 | msgid "Show remote label" 228 | msgstr "" 229 | 230 | #: prefs.js:297 231 | msgid "Receiver name as label" 232 | msgstr "" 233 | 234 | #: prefs.js:347 235 | msgid "Chromecast Options" 236 | msgstr "" 237 | 238 | #: prefs.js:351 prefs.js:663 239 | msgid "Device selection" 240 | msgstr "" 241 | 242 | #: prefs.js:378 243 | msgid "Subtitles" 244 | msgstr "" 245 | 246 | #: prefs.js:382 247 | msgid "Font family" 248 | msgstr "" 249 | 250 | #: prefs.js:401 251 | msgid "Font style" 252 | msgstr "" 253 | 254 | #. TRANSLATORS: One of "Speed" setting value 255 | #: prefs.js:403 node_scripts/messages.js:22 256 | msgid "Normal" 257 | msgstr "" 258 | 259 | #: prefs.js:404 260 | msgid "Bold" 261 | msgstr "" 262 | 263 | #: prefs.js:405 264 | msgid "Italic" 265 | msgstr "" 266 | 267 | #: prefs.js:406 268 | msgid "Bold italic" 269 | msgstr "" 270 | 271 | #: prefs.js:416 272 | msgid "Scale factor" 273 | msgstr "" 274 | 275 | #: prefs.js:430 276 | msgid "Font color" 277 | msgstr "" 278 | 279 | #: prefs.js:442 280 | msgid "Font outline" 281 | msgstr "" 282 | 283 | #: prefs.js:472 284 | msgid "Background color" 285 | msgstr "" 286 | 287 | #: prefs.js:540 288 | msgid "Encoder" 289 | msgstr "" 290 | 291 | #: prefs.js:545 292 | msgid "Media Encoding" 293 | msgstr "" 294 | 295 | #: prefs.js:549 296 | msgid "Hardware acceleration" 297 | msgstr "" 298 | 299 | #: prefs.js:551 prefs.js:1141 300 | msgid "None" 301 | msgstr "" 302 | 303 | #: prefs.js:558 304 | msgid "Bitrate (Mbps)" 305 | msgstr "" 306 | 307 | #: prefs.js:568 308 | msgid "Burn subtitles when transcoding video" 309 | msgstr "" 310 | 311 | #. TRANSLATORS: "Players" as video players 312 | #: prefs.js:583 313 | msgid "Extractor" 314 | msgstr "" 315 | 316 | #: prefs.js:588 317 | msgid "Subtitles Extraction" 318 | msgstr "" 319 | 320 | #. TRANSLATORS: "vttextract" is the name of executable, do not change 321 | #: prefs.js:593 322 | msgid "Add vttextract executable" 323 | msgstr "" 324 | 325 | #: prefs.js:602 326 | msgid "Reuse extracted subtitles" 327 | msgstr "" 328 | 329 | #. TRANSLATORS: Destination folder to save subtitles 330 | #: prefs.js:611 331 | msgid "Save folder" 332 | msgstr "" 333 | 334 | #: prefs.js:613 335 | msgid "Select folder" 336 | msgstr "" 337 | 338 | #: prefs.js:654 339 | msgid "Misc" 340 | msgstr "" 341 | 342 | #: prefs.js:676 343 | msgid "Web Player" 344 | msgstr "" 345 | 346 | #: prefs.js:680 347 | msgid "Subtitles scale factor" 348 | msgstr "" 349 | 350 | #. TRANSLATORS: The rest of extension settings 351 | #: prefs.js:691 352 | msgid "Miscellaneous" 353 | msgstr "" 354 | 355 | #: prefs.js:695 356 | msgid "Music visualizer" 357 | msgstr "" 358 | 359 | #: prefs.js:703 360 | msgid "Nautilus/Nemo integration" 361 | msgstr "" 362 | 363 | #: prefs.js:809 364 | msgid "Install npm modules" 365 | msgstr "" 366 | 367 | #: prefs.js:833 368 | msgid "Installing..." 369 | msgstr "" 370 | 371 | #: prefs.js:887 372 | msgid "version:" 373 | msgstr "" 374 | 375 | #: prefs.js:894 376 | msgid "Developed by" 377 | msgstr "" 378 | 379 | #: prefs.js:903 380 | msgid "Extension Homepage" 381 | msgstr "" 382 | 383 | #: prefs.js:910 384 | msgid "Playercast Homepage" 385 | msgstr "" 386 | 387 | #: prefs.js:917 388 | msgid "Donate" 389 | msgstr "" 390 | 391 | #: prefs.js:931 392 | msgid "Main" 393 | msgstr "" 394 | 395 | #: prefs.js:935 396 | msgid "Remote" 397 | msgstr "" 398 | 399 | #. TRANSLATORS: Other extension settings 400 | #: prefs.js:944 401 | msgid "Other" 402 | msgstr "" 403 | 404 | #: prefs.js:957 405 | msgid "Add-ons" 406 | msgstr "" 407 | 408 | #: prefs.js:962 409 | msgid "Modules" 410 | msgstr "" 411 | 412 | #: prefs.js:966 413 | msgid "About" 414 | msgstr "" 415 | 416 | #: prefs.js:1074 417 | msgid "Manual IP Config" 418 | msgstr "" 419 | 420 | #: prefs.js:1131 421 | msgid "Auto" 422 | msgstr "" 423 | 424 | #: prefs.js:1132 425 | msgid "Name" 426 | msgstr "" 427 | 428 | #. TRANSLATORS: Text field temporary text 429 | #: prefs.js:1148 430 | msgid "Insert name" 431 | msgstr "" 432 | 433 | #. TRANSLATORS: Shown when scan for Chromecast devices is running 434 | #: prefs.js:1263 435 | msgid "Scanning..." 436 | msgstr "" 437 | 438 | #: node_scripts/messages.js:4 439 | msgid "LOADING" 440 | msgstr "" 441 | 442 | #: node_scripts/messages.js:5 443 | msgid "No file selected" 444 | msgstr "" 445 | 446 | #: node_scripts/messages.js:6 447 | msgid "Receiver type is set to Chromecast" 448 | msgstr "" 449 | 450 | #: node_scripts/messages.js:7 451 | msgid "Receiver type is set to Playercast app" 452 | msgstr "" 453 | 454 | #: node_scripts/messages.js:8 455 | msgid "Streaming process is still active" 456 | msgstr "" 457 | 458 | #. TRANSLATORS: Do not remove HTML tags 459 | #: node_scripts/messages.js:10 460 | msgid "Too many connections!

Close all other tabs that are accessing this page in all browsers

" 461 | msgstr "" 462 | 463 | #. TRANSLATORS: This sentence will contain file path after end 464 | #: node_scripts/messages.js:12 465 | msgid "FFmpeg could not transcode file:" 466 | msgstr "" 467 | 468 | #: node_scripts/messages.js:13 469 | msgid "FFmpeg path is incorrect" 470 | msgstr "" 471 | 472 | #. TRANSLATORS: This sentence will contain file path after end 473 | #: node_scripts/messages.js:15 474 | msgid "FFprobe could not process file:" 475 | msgstr "" 476 | 477 | #: node_scripts/messages.js:16 478 | msgid "FFprobe path is incorrect" 479 | msgstr "" 480 | 481 | #. TRANSLATORS: This sentence will contain file path after end 482 | #: node_scripts/messages.js:18 483 | msgid "Could not analyze selected file:" 484 | msgstr "" 485 | 486 | #: node_scripts/messages.js:20 487 | msgid "Speed" 488 | msgstr "" 489 | 490 | #: node_scripts/messages.js:25 491 | msgid "Device not found" 492 | msgstr "" 493 | 494 | #: node_scripts/messages.js:26 495 | msgid "Failed to load media" 496 | msgstr "" 497 | 498 | #: node_scripts/messages.js:27 499 | msgid "Could not connect to device" 500 | msgstr "" 501 | 502 | #: node_scripts/messages.js:28 503 | msgid "Verify device IP in extension preferences" 504 | msgstr "" 505 | 506 | #. TRANSLATORS: This sentence will contain file path after end 507 | #: node_scripts/messages.js:30 508 | msgid "Could not play file:" 509 | msgstr "" 510 | 511 | #: node_scripts/messages.js:31 512 | msgid "Try again with transcoding enabled" 513 | msgstr "" 514 | 515 | -------------------------------------------------------------------------------- /prefs_shared.js: -------------------------------------------------------------------------------- 1 | imports.gi.versions.Gtk = '3.0'; 2 | 3 | const { Gtk } = imports.gi; 4 | let gridIndex = 0; 5 | 6 | var SettingLabel = class SharedSettingLabel 7 | { 8 | constructor(text, isTitle, isTopMargin) 9 | { 10 | let label = null; 11 | let marginLR = 0; 12 | let marginTop = 0; 13 | 14 | if(isTitle) label = '' + text + ''; 15 | else 16 | { 17 | label = text; 18 | marginLR = 12; 19 | } 20 | 21 | if(isTopMargin) marginTop = 20; 22 | 23 | return new Gtk.Label({ 24 | label: label, 25 | use_markup: true, 26 | hexpand: true, 27 | halign: Gtk.Align.START, 28 | margin_top: marginTop, 29 | margin_left: marginLR, 30 | margin_right: marginLR 31 | }); 32 | } 33 | } 34 | 35 | function addToGrid(grid, leftWidget, rightWidget, resetIndex) 36 | { 37 | if(resetIndex) gridIndex = 0; 38 | if(leftWidget) grid.attach(leftWidget, 0, gridIndex, 1, 1); 39 | if(rightWidget) grid.attach(rightWidget, 1, gridIndex, 1, 1); 40 | 41 | gridIndex++; 42 | } 43 | -------------------------------------------------------------------------------- /schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rafostar/gnome-shell-extension-cast-to-tv/10502fc3cd247f776895d418585243477de23e24/schemas/gschemas.compiled -------------------------------------------------------------------------------- /schemas/org.gnome.shell.extensions.cast-to-tv.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | "chromecast" 6 | Device receiving media 7 | 8 | 9 | "/usr/bin/ffmpeg" 10 | Custom path to FFmpeg 11 | 12 | 13 | "/usr/bin/ffprobe" 14 | Custom path to FFprobe 15 | 16 | 17 | 4000 18 | Port used for incoming connections 19 | 20 | 21 | 9200 22 | Port used for internal communication 23 | 24 | 25 | 26 | "right" 27 | Position of chromecast remote menu button 28 | 29 | 30 | 10 31 | Time amount to skip when seeking 32 | 33 | 34 | 30 35 | Time amount after which slideshow image changes 36 | 37 | 38 | true 39 | Unify volume and progress sliders 40 | 41 | 42 | true 43 | Show label next to remote controller 44 | 45 | 46 | false 47 | Show receiver friendly name as remote controller label 48 | 49 | 50 | 20 51 | Icon size for widget media control buttons 52 | 53 | 54 | 16 55 | Icon size for widget slider 56 | 57 | 58 | 59 | "" 60 | Selected Chromecast device name 61 | 62 | 63 | "{}" 64 | Custom Chromecast subtitles config 65 | 66 | 67 | "[]" 68 | Scanned Chromecast devices list 69 | 70 | 71 | 72 | "" 73 | Playercast device name 74 | 75 | 76 | "[]" 77 | Scanned Playercast devices list 78 | 79 | 80 | 81 | "none" 82 | Video streaming hardware acceleration 83 | 84 | 85 | 5.0 86 | Media encoding video bitrate 87 | 88 | 89 | false 90 | Burn subtitles into video during transcode 91 | 92 | 93 | 94 | true 95 | Enable reusing subtitles from extractor dir 96 | 97 | 98 | "/tmp" 99 | Extractor save folder 100 | 101 | 102 | "eng/English" 103 | Preferred subtitles language 104 | 105 | 106 | "" 107 | Fallback subtitles language 108 | 109 | 110 | 111 | false 112 | FFmpeg music visualizer 113 | 114 | 115 | 1.0 116 | Web player subtitles size 117 | 118 | 119 | 120 | false 121 | Remember last service state 122 | 123 | 124 | 125 | 0 126 | Restore previous transcode option in file chooser 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /server-monitor.js: -------------------------------------------------------------------------------- 1 | const { Gio, GLib } = imports.gi; 2 | const Settings = new Gio.Settings({ schema: 'org.gnome.shell' }); 3 | 4 | const EXTENSION_NAME = 'cast-to-tv@rafostar.github.com'; 5 | const LOCAL_PATH = GLib.get_current_dir(); 6 | const NODE_PATH = (GLib.find_program_in_path('nodejs') || GLib.find_program_in_path('node')); 7 | 8 | imports.searchPath.unshift(LOCAL_PATH); 9 | const Helper = imports.helper; 10 | const Soup = imports.soup; 11 | imports.searchPath.shift(); 12 | 13 | const CastSettings = Helper.getSettings(LOCAL_PATH); 14 | 15 | let statusTimer; 16 | let restartCount = 0; 17 | let persistent = true; 18 | let loop = GLib.MainLoop.new(null, false); 19 | 20 | class ServerMonitor 21 | { 22 | constructor() 23 | { 24 | let canStart = (this._getIsExtensionEnabled() && !this._getIsServerRunning()); 25 | if(!canStart) return; 26 | 27 | if(!NODE_PATH) 28 | Helper.notify('Cast to TV', 'nodejs' + ' ' + "is not installed!"); 29 | 30 | if( 31 | !NODE_PATH 32 | || !this._checkModules() 33 | || !this._checkAddons() 34 | ) { 35 | /* If there was an error do not try to start on each login */ 36 | if(CastSettings.get_boolean('service-wanted')) 37 | CastSettings.set_boolean('service-wanted', false); 38 | 39 | return; 40 | } 41 | 42 | Settings.connect('changed::disable-user-extensions', () => this._onSettingsChanged()); 43 | Settings.connect('changed::enabled-extensions', () => this._onSettingsChanged()); 44 | 45 | this.startServer(); 46 | loop.run(); 47 | } 48 | 49 | startServer() 50 | { 51 | let proc = Gio.Subprocess.new( 52 | [NODE_PATH, `${LOCAL_PATH}/node_scripts/server`], 53 | Gio.SubprocessFlags.NONE 54 | ); 55 | log('Cast to TV: service started'); 56 | 57 | proc.wait_async(null, () => 58 | { 59 | loop.quit(); 60 | 61 | if(persistent) 62 | { 63 | log('Cast to TV: restarting server'); 64 | 65 | let modulesInstalled = this._checkModules(); 66 | let addonsInstalled = this._checkAddons(); 67 | 68 | if(modulesInstalled && addonsInstalled) 69 | { 70 | this.startServer(); 71 | 72 | restartCount++; 73 | if(restartCount >= 3) 74 | { 75 | log('Cast to TV: server crashed too many times!'); 76 | return this.stopServer(); 77 | } 78 | 79 | if(!statusTimer) 80 | this._startTimer(); 81 | 82 | return loop.run(); 83 | } 84 | } 85 | 86 | log('Cast to TV: service stopped'); 87 | }); 88 | } 89 | 90 | stopServer() 91 | { 92 | persistent = false; 93 | GLib.spawn_command_line_sync(`pkill -SIGINT -f ${LOCAL_PATH}/node_scripts/server`); 94 | } 95 | 96 | _getIsExtensionEnabled() 97 | { 98 | let allDisabled = Settings.get_boolean('disable-user-extensions'); 99 | if(!allDisabled) 100 | { 101 | let enabledExtensions = Settings.get_strv('enabled-extensions'); 102 | 103 | if(enabledExtensions.includes(EXTENSION_NAME)) 104 | return true; 105 | } 106 | 107 | log('Cast to TV: extension is disabled'); 108 | 109 | return false; 110 | } 111 | 112 | _getIsServerRunning() 113 | { 114 | if(!Soup.client) 115 | Soup.createClient(CastSettings.get_int('listening-port')); 116 | 117 | let data = Soup.client.getIsServiceEnabledSync(); 118 | 119 | return (data && data.isEnabled) ? true : false; 120 | } 121 | 122 | _checkModules(sourceDir) 123 | { 124 | sourceDir = sourceDir || LOCAL_PATH; 125 | 126 | let modulesPath = `${sourceDir}/node_modules`; 127 | 128 | /* Read cast-to-tv package.json */ 129 | let pkgInfo = this._getPkgInfo(sourceDir); 130 | 131 | if(!pkgInfo || !pkgInfo.dependencies) 132 | { 133 | Helper.notify('Cast to TV', `Unreadable package.json in "${sourceDir}"`); 134 | return false; 135 | } 136 | 137 | /* Modules check should be after package.json read */ 138 | let folderExists = GLib.file_test(modulesPath, GLib.FileTest.EXISTS); 139 | if(!folderExists) 140 | { 141 | log(`Cast to TV: missing folder "${modulesPath}"`); 142 | Helper.notify('Cast to TV', 'Required npm modules are not installed!'); 143 | return false; 144 | } 145 | 146 | let dependencies = pkgInfo.dependencies; 147 | 148 | for(let module in dependencies) 149 | { 150 | let modulePath = `${modulesPath}/${module}`; 151 | let moduleExists = GLib.file_test(modulePath, GLib.FileTest.EXISTS); 152 | 153 | if(!moduleExists) 154 | { 155 | Helper.notify('Cast to TV', `Missing npm module: ${module}`); 156 | return false; 157 | } 158 | 159 | let isRequiredVer = this._checkPkgVersion(modulePath, dependencies[module]); 160 | 161 | if(!isRequiredVer) 162 | { 163 | Helper.notify('Cast to TV', 'Installed npm modules are outdated!'); 164 | return false; 165 | } 166 | } 167 | 168 | return true; 169 | } 170 | 171 | _checkAddons() 172 | { 173 | let extPath = LOCAL_PATH.substring(0, LOCAL_PATH.lastIndexOf('/')); 174 | let extDir = Gio.File.new_for_path(extPath); 175 | let dirEnum = extDir.enumerate_children('standard::name,standard::type', 0, null); 176 | let addons = []; 177 | 178 | let info; 179 | while((info = dirEnum.next_file(null))) 180 | { 181 | let dirName = info.get_name(); 182 | 183 | if(dirName.includes('cast-to-tv') && dirName.includes('addon')) 184 | addons.push(`${extPath}/${dirName}`); 185 | } 186 | 187 | for(let addonDir of addons) 188 | { 189 | let addonModulesInstalled = this._checkModules(addonDir); 190 | 191 | if(!addonModulesInstalled) 192 | return false; 193 | } 194 | 195 | return true; 196 | } 197 | 198 | _checkPkgVersion(modulePath, version) 199 | { 200 | let isExactVer = false; 201 | let pkgInfo = this._getPkgInfo(modulePath); 202 | 203 | if(!pkgInfo || !pkgInfo.version) 204 | return false; 205 | 206 | if(isNaN(version.charAt(0))) 207 | version = version.substring(1); 208 | else 209 | isExactVer = true; 210 | 211 | return (isExactVer) 212 | ? pkgInfo.version == version 213 | : pkgInfo.version >= version 214 | } 215 | 216 | _getPkgInfo(modulePath) 217 | { 218 | let data = Helper.readFromFile(`${modulePath}/package.json`); 219 | return (data) ? data : null; 220 | } 221 | 222 | _onSettingsChanged() 223 | { 224 | let enabled = this._getIsExtensionEnabled(); 225 | 226 | if(!enabled) 227 | this.stopServer(); 228 | } 229 | 230 | _startTimer() 231 | { 232 | statusTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 30, () => 233 | { 234 | statusTimer = null; 235 | restartCount = 0; 236 | 237 | return GLib.SOURCE_REMOVE; 238 | }); 239 | } 240 | } 241 | 242 | let monitor = new ServerMonitor(); 243 | -------------------------------------------------------------------------------- /shared.js: -------------------------------------------------------------------------------- 1 | if(typeof process === 'undefined') 2 | { 3 | /* When importing to GJS */ 4 | var exports = {}; 5 | var module = {exports}; 6 | } 7 | 8 | var tempDir = '/tmp/.cast-to-tv'; 9 | 10 | module.exports = { 11 | tempDir: tempDir, 12 | hlsDir: tempDir + '/stream', 13 | vttSubsPath: tempDir + '/webplayer_subs.vtt', 14 | coverDefault: tempDir + '/cover', 15 | escapeChars: [' ', '[', ']', '"', "'"], 16 | coverNames: ['cover', 'cover_01', 'cover 01', 'cover1'], 17 | coverExtensions: ['.jpg', '.png'], 18 | subsFormats: ['srt', 'ass', 'vtt'], 19 | chromecast: { 20 | videoBuffer: 2500, 21 | visualizerBuffer: 6500, 22 | subsStyle: { 23 | backgroundColor: '#00000000', 24 | foregroundColor: '#FFFFFFFF', 25 | edgeType: 'OUTLINE', 26 | edgeColor: '#000000FF', 27 | fontScale: '1.0', 28 | fontStyle: 'NORMAL', 29 | fontFamily: 'Droid Sans', 30 | fontGenericFamily: 'SANS_SERIF', 31 | windowType: 'NONE' 32 | }, 33 | tracks: [{ 34 | trackId: 1, 35 | type: 'TEXT', 36 | trackContentType: 'text/vtt', 37 | name: 'Subtitles', 38 | subtype: 'SUBTITLES' 39 | }] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /temp.js: -------------------------------------------------------------------------------- 1 | const { GLib } = imports.gi; 2 | const Local = imports.misc.extensionUtils.getCurrentExtension(); 3 | const Settings = Local.imports.helper.getSettings(Local.path); 4 | 5 | function getConfig() 6 | { 7 | /* Get only settings required by extension */ 8 | let config = { 9 | listeningPort: Settings.get_int('listening-port'), 10 | internalPort: Settings.get_int('internal-port'), 11 | musicVisualizer: Settings.get_boolean('music-visualizer') 12 | }; 13 | 14 | return config; 15 | } 16 | 17 | function getRemoteOpts() 18 | { 19 | let opts = { 20 | mode: 'DIRECT', 21 | seekTime: Settings.get_int('seek-time'), 22 | isUnifiedSlider: Settings.get_boolean('unified-slider'), 23 | isLabel: Settings.get_boolean('remote-label'), 24 | receiverType: Settings.get_string('receiver-type'), 25 | useFriendlyName: Settings.get_boolean('remote-label-fn'), 26 | sliderIconSize: Settings.get_int('slider-icon-size'), 27 | mediaButtonsSize: Settings.get_int('media-buttons-size') 28 | }; 29 | 30 | return opts; 31 | } 32 | -------------------------------------------------------------------------------- /webplayer/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rafostar/gnome-shell-extension-cast-to-tv/10502fc3cd247f776895d418585243477de23e24/webplayer/images/cover.png -------------------------------------------------------------------------------- /webplayer/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rafostar/gnome-shell-extension-cast-to-tv/10502fc3cd247f776895d418585243477de23e24/webplayer/images/icon.png -------------------------------------------------------------------------------- /webplayer/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rafostar/gnome-shell-extension-cast-to-tv/10502fc3cd247f776895d418585243477de23e24/webplayer/images/play.png -------------------------------------------------------------------------------- /webplayer/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cast to TV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

18 |
19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /webplayer/loading.js: -------------------------------------------------------------------------------- 1 | var websocket = io(); 2 | websocket.once('connect', onWebsocketConnect); 3 | websocket.on('loading-text', onLoadingText); 4 | websocket.on('processes-done', onProcessesDone); 5 | 6 | function onWebsocketConnect() 7 | { 8 | websocket.emit('webplayer', 'loading-ask'); 9 | } 10 | 11 | function onLoadingText(text) 12 | { 13 | document.getElementById('msg').innerHTML = text; 14 | websocket.emit('webplayer', 'processes-ask'); 15 | } 16 | 17 | function onProcessesDone(isDone) 18 | { 19 | if(!isDone) return; 20 | 21 | websocket.disconnect(); 22 | setTimeout(() => location.reload(true), 250); 23 | } 24 | -------------------------------------------------------------------------------- /webplayer/message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cast to TV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

17 |
18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /webplayer/message.js: -------------------------------------------------------------------------------- 1 | var websocket = io(); 2 | websocket.once('connect', onWebsocketConnect); 3 | websocket.on('message-refresh', refreshMessage); 4 | websocket.on('message-clear', changePage); 5 | var msgCheckInterval = null; 6 | 7 | function onWebsocketConnect() 8 | { 9 | websocket.emit('webplayer', 'message-ask'); 10 | msgCheckInterval = setInterval(() => { websocket.emit('webplayer', 'message-ask'); }, 1000); 11 | } 12 | 13 | function refreshMessage(msg) 14 | { 15 | if(document.getElementById("msg").innerHTML != msg) 16 | document.getElementById("msg").innerHTML = msg; 17 | } 18 | 19 | function changePage() 20 | { 21 | if(msgCheckInterval) 22 | clearInterval(msgCheckInterval); 23 | 24 | websocket.disconnect(); 25 | setTimeout(() => location.reload(true), 250); 26 | } 27 | -------------------------------------------------------------------------------- /webplayer/picture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cast to TV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /webplayer/preload.js: -------------------------------------------------------------------------------- 1 | function makeID() 2 | { 3 | var text = ""; 4 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 5 | 6 | for(var i = 0; i < 10; i++) 7 | { 8 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 9 | } 10 | 11 | return text; 12 | } 13 | -------------------------------------------------------------------------------- /webplayer/webplayer-defaults.js: -------------------------------------------------------------------------------- 1 | var playerOptions = { 2 | controls: ['play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'fullscreen'], 3 | settings: ['speed'], 4 | clickToPlay: false, 5 | invertTime: false, 6 | toggleInvert: true, 7 | keyboard: { focused: true, global: true }, 8 | captions: { active: true, language: 'en', update: false }, 9 | iconUrl: '/plyr/plyr.svg', 10 | blankVideo: '/plyr/blank.mp4', 11 | storage: { enabled: true, key: 'cast-to-tv' } 12 | }; 13 | -------------------------------------------------------------------------------- /webplayer/webplayer-encode.js: -------------------------------------------------------------------------------- 1 | playerOptions.controls = ['play', 'current-time', 'mute', 'volume', 'captions', 'pip', 'fullscreen']; 2 | playerOptions.toggleInvert = false; 3 | -------------------------------------------------------------------------------- /webplayer/webplayer-init.js: -------------------------------------------------------------------------------- 1 | const isMobile = (/Android|iPhone|iPad|iPod|BlackBerry|webOS|IEMobile|Windows Phone/i.test(navigator.userAgent)) ? true : false; 2 | const player = new Plyr('#player', playerOptions); 3 | 4 | function preparePlayer(msg) 5 | { 6 | var sessionID = makeID(); 7 | var posterPath = '/webplayer/images/play.png'; 8 | var subsKind = 'none'; 9 | var subsSrc = null; 10 | 11 | player.config.i18n = msg.i18n; 12 | 13 | /* Show album cover when playing without visualizations */ 14 | if(msg.type == 'MUSIC') 15 | { 16 | posterPath = '/cover?session=' + sessionID; 17 | } 18 | else if(msg.subs) 19 | { 20 | /* Enable subtitles */ 21 | subsKind = 'captions'; 22 | subsSrc = '/subswebplayer?session=' + sessionID; 23 | } 24 | 25 | player.source = { 26 | type: 'video', 27 | title: 'Cast to TV', 28 | sources: [{ 29 | src: '/cast?session=' + sessionID, 30 | type: 'video/mp4' 31 | }], 32 | poster: posterPath, 33 | tracks: [{ 34 | kind: subsKind, 35 | label: 'Subtitles', 36 | srclang: 'en', 37 | src: subsSrc, 38 | default: true 39 | }] 40 | }; 41 | } 42 | 43 | function addClickListeners() 44 | { 45 | /* Toggle play on click event listener */ 46 | var div = document.getElementsByClassName('plyr__video-wrapper')[0]; 47 | div.addEventListener('click', startPlayer); 48 | } 49 | 50 | function startPlayer() 51 | { 52 | /* When on mobile */ 53 | if(isMobile) 54 | { 55 | if(!player.fullscreen.active) 56 | { 57 | /* Enter fullscreen after touch (when paused) */ 58 | player.fullscreen.enter(); 59 | 60 | if(!player.playing) player.togglePlay(); 61 | return; 62 | } 63 | } 64 | 65 | /* Play and pause on click/touch */ 66 | player.togglePlay(); 67 | } 68 | -------------------------------------------------------------------------------- /webplayer/webplayer.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #222222; 3 | margin: 0; 4 | } 5 | 6 | .plyr__captions { 7 | bottom: 2vh; 8 | } 9 | 10 | .plyr__captions .plyr__caption { 11 | background: rgba(0,0,0,0); 12 | text-shadow: -1px -1px 1px #000000, -1px 1px 1px #000000, 1px -1px 1px #000000, 1px 1px 1px #000000; 13 | font-family: sans-serif; 14 | font-weight: bold; 15 | line-height: 1.0; 16 | 17 | -webkit-touch-callout: none; 18 | -webkit-user-select: none; 19 | -khtml-user-select: none; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | user-select: none; 23 | } 24 | 25 | .plyr__time { 26 | cursor: pointer; 27 | 28 | -webkit-touch-callout: none; 29 | -webkit-user-select: none; 30 | -khtml-user-select: none; 31 | -moz-user-select: none; 32 | -ms-user-select: none; 33 | user-select: none; 34 | } 35 | 36 | .plyr video { 37 | height: 100vh; 38 | } 39 | 40 | .cast-picture { 41 | display: block; 42 | height: 100vh; 43 | } 44 | 45 | .screen-center { 46 | margin: 0; 47 | position: absolute; 48 | top: 50%; 49 | left: 50%; 50 | 51 | -webkit-transform: translateX(-50%) translateY(-50%); 52 | -khtml-transform: translateX(-50%) translateY(-50%); 53 | -moz--transform: translateX(-50%) translateY(-50%); 54 | -ms-transform: translateX(-50%) translateY(-50%); 55 | transform: translateX(-50%) translateY(-50%); 56 | } 57 | 58 | #msg { 59 | color: white; 60 | 61 | -webkit-touch-callout: none; 62 | -webkit-user-select: none; 63 | -khtml-user-select: none; 64 | -moz-user-select: none; 65 | -ms-user-select: none; 66 | user-select: none; 67 | } 68 | 69 | .load-text { 70 | margin-top: 10px; 71 | margin-left: 2px; 72 | position: relative; 73 | font-family: 'Sniglet', cursive; 74 | font-weight: bold; 75 | font-size: 52px; 76 | } 77 | 78 | .msg-text { 79 | margin-top: 10px; 80 | margin-left: 2px; 81 | position: relative; 82 | font-family: 'Sniglet', cursive; 83 | font-weight: bold; 84 | font-size: 36px; 85 | } 86 | 87 | .loader { 88 | border: 20px solid white; 89 | border-radius: 50%; 90 | border-top: 20px solid #3498db; 91 | width: 80px; 92 | height: 80px; 93 | 94 | -webkit-animation: spin 0.8s linear infinite; 95 | -khtml-animation: spin 0.8s linear infinite; 96 | -moz-animation: spin 0.8s linear infinite; 97 | -ms-animation: spin 0.8s linear infinite; 98 | animation: spin 0.8s linear infinite; 99 | } 100 | 101 | @-webkit-keyframes spin { 102 | 0% { -webkit-transform: rotate(0deg); } 103 | 100% { -webkit-transform: rotate(360deg); } 104 | } 105 | 106 | @-khtml-keyframes spin { 107 | 0% { -webkit-transform: rotate(0deg); } 108 | 100% { -webkit-transform: rotate(360deg); } 109 | } 110 | 111 | @-moz-keyframes spin { 112 | 0% { -webkit-transform: rotate(0deg); } 113 | 100% { -webkit-transform: rotate(360deg); } 114 | } 115 | 116 | @-ms-keyframes spin { 117 | 0% { -webkit-transform: rotate(0deg); } 118 | 100% { -webkit-transform: rotate(360deg); } 119 | } 120 | 121 | @keyframes spin { 122 | 0% { transform: rotate(0deg); } 123 | 100% { transform: rotate(360deg); } 124 | } 125 | -------------------------------------------------------------------------------- /webplayer/webplayer_direct.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cast to TV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /webplayer/webplayer_encode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cast to TV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /webplayer/websocket.js: -------------------------------------------------------------------------------- 1 | var websocket = io(); 2 | websocket.once('connect', onWebsocketConnect); 3 | 4 | var statusContents = { 5 | playerState: 'PAUSED', 6 | currentTime: 0, 7 | media: { duration: 0 }, 8 | volume: getVolume() 9 | }; 10 | 11 | function getVolume() 12 | { 13 | if(typeof player === 'undefined' || player.muted) 14 | return 0; 15 | else 16 | return player.volume; 17 | } 18 | 19 | function onWebsocketConnect() 20 | { 21 | /* Web player related websocket functions */ 22 | if(typeof player !== 'undefined') 23 | { 24 | var progress = 0; 25 | var playbackStarted = false; 26 | 27 | websocket.emit('webplayer', 'webplayer-ask'); 28 | 29 | websocket.on('webplayer-init', msg => 30 | { 31 | preparePlayer(msg); 32 | addClickListeners(); 33 | }); 34 | player.on('ended', () => websocket.emit('webplayer', 'track-ended')); 35 | 36 | player.on('loadeddata', () => 37 | { 38 | /* Workaround Plyr volume bug */ 39 | player.currentTime = 0; 40 | 41 | statusContents.media.duration = player.duration; 42 | }); 43 | 44 | player.on('canplay', () => 45 | { 46 | if(!playbackStarted) player.play(); 47 | }); 48 | 49 | player.on('playing', () => 50 | { 51 | playbackStarted = true; 52 | statusContents.playerState = 'PLAYING'; 53 | websocket.emit('status-update', statusContents); 54 | }); 55 | 56 | player.on('pause', () => 57 | { 58 | statusContents.playerState = 'PAUSED'; 59 | websocket.emit('status-update', statusContents); 60 | }); 61 | 62 | player.on('seeked', () => 63 | { 64 | progress = 0; 65 | statusContents.currentTime = player.currentTime; 66 | websocket.emit('status-update', statusContents); 67 | }); 68 | 69 | player.on('volumechange', () => 70 | { 71 | statusContents.volume = getVolume(); 72 | websocket.emit('status-update', statusContents); 73 | }); 74 | 75 | setInterval(() => 76 | { 77 | statusContents.currentTime = player.currentTime; 78 | websocket.emit('status-update', statusContents); 79 | }, 1000); 80 | 81 | websocket.on('remote-signal', msg => 82 | { 83 | switch(msg.action) 84 | { 85 | case 'PLAY': 86 | player.play(); 87 | break; 88 | case 'PAUSE': 89 | player.pause(); 90 | break; 91 | case 'SEEK': 92 | player.currentTime = statusContents.media.duration * msg.value; 93 | break; 94 | case 'SEEK+': 95 | player.forward(msg.value); 96 | break; 97 | case 'SEEK-': 98 | player.rewind(msg.value); 99 | break; 100 | case 'VOLUME': 101 | player.volume = msg.value; 102 | break; 103 | case 'STOP': 104 | player.stop(); 105 | break; 106 | default: 107 | break; 108 | } 109 | }); 110 | } 111 | else 112 | { 113 | websocket.emit('show-remote', true); 114 | websocket.emit('status-update', statusContents); 115 | } 116 | } 117 | 118 | websocket.on('reload', () => 119 | { 120 | websocket.disconnect(); 121 | 122 | /* Temporary solution for changing web page before 123 | nodejs reduces active connections number */ 124 | setTimeout(() => location.reload(true), 250); 125 | }); 126 | --------------------------------------------------------------------------------