diff --git a/README.md b/README.md index 6f8aa80..7d70441 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,17 @@ # OctoPrint-YouTubeLive +**Overview:** Plugin that adds a tab to OctoPrint for viewing, starting, and stopping a YouTube Live stream. -**Overview:** A simple youtube live viewer tab for OctoPrint. Currently a work in progress and simple skeleton. - -**Details:** This plugin in combination with the instructions found [here](https://blog.alexellis.io/live-stream-with-docker/) creates a pretty good streaming solution for OctoPrint. Currently tested with Octoprint running on Raspberry Pi Zero W and Google Chrome. - -Would probably run better on a Pi 3 as the live stream does start to buffer running both OctoPrint and the stream from the same Pi Zero W. You could run your stream from any device, this plugin just creates the tab to allow you to look at the given channels live stream. +**Details:** Based on the work found [here](https://blog.alexellis.io/live-stream-with-docker/). Currently tested with OctoPrint running on a Raspberry Pi Zero W and on a Pi3. -## Setup +## Requirements +Follow the instructions found [here](docker_instructions.md) to install and configure docker/mmjpeg for use with this plugin. -Once installed enter your YouTube's channel id which can be found on your [Advanced Account Settings](https://www.youtube.com/account_advanced). Enter your YouTube Channel ID into the YouTube Live settings within OctoPrint's settings dialog. +## Setup +Once installed enter your YouTube's channel id ([Advanced Account Settings](https://www.youtube.com/account_advanced)) and your YouTube stream id ([YouTube Live Dashboard](https://www.youtube.com/live_dashboard)) into the YouTube Live plugin settings. -**Note:** If you use the same pi for both streaming and octoprint you will need to stop the webcamd service if using an OctoPi distribution. I ended up using the [SystemCommandEditor](https://github.com/Salandora/OctoPrint-SystemCommandEditor) plugin and added the following commands: - -+ sudo service webcamd stop && docker run --privileged --name cam -d alexellis2/streaming:17-5-2017 *xxxx-xxxx-xxxx-xxxx* -+ docker stop cam && docker rm cam && sudo service webcamd start - -manually: - - sudo pip install https://github.com/jneilliii/OctoPrint-YouTubeLive/archive/master.zip - -## Configuration - ## TODO: * [ ] Additional testing. diff --git a/docker_instructions.md b/docker_instructions.md new file mode 100644 index 0000000..e71253e --- /dev/null +++ b/docker_instructions.md @@ -0,0 +1,32 @@ +**Install docker** + + curl -sSL https://get.docker.com | sh + sudo usermod pi -aG docker + sudo reboot + +**Pull Docker Image** + + docker pull alexellis2/streaming:17-5-2017 + +**Clone Repository and Rebuild** + + cd ~ + git clone https://github.com/jneilliii/youtubelive --depth 1 + cd youtubelive + docker build -t octoprint/youtubelive . + +**Test** + +Set up your stream on the [YouTube Live Dashboard](https://www.youtube.com/live_dashboard) and enter your stream id in the command below in place of xxxx-xxxx-xxxx-xxxx. + + docker run --privileged --name YouTubeLive -ti octoprint/youtubelive:latest http://localhost:8080/?action=stream xxxx-xxxx-xxxx-xxxx + +Stream should go live and re-encode the OctoPrint stream to YouTube. Once verified close ffmpeg and remove docker container. + + ctrl+c + docker rm YouTubeLive + +**OctoPrint Settings** + +Enter your stream id used above in the OctoPrint-YouTubeLive plugin settings. + diff --git a/octoprint_youtubelive/__init__.py b/octoprint_youtubelive/__init__.py index f493b47..c48fbb5 100644 --- a/octoprint_youtubelive/__init__.py +++ b/octoprint_youtubelive/__init__.py @@ -2,15 +2,29 @@ from __future__ import absolute_import import octoprint.plugin +from octoprint.server import user_permission +import docker class youtubelive(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, - octoprint.plugin.SettingsPlugin): + octoprint.plugin.SettingsPlugin, + octoprint.plugin.SimpleApiPlugin): + + def __init__(self): + self.client = docker.from_env() + self.container = None ##~~ StartupPlugin def on_after_startup(self): - self._logger.info("OctoPrint-YouTubeLive loaded!") + self._logger.info("OctoPrint-YouTubeLive loaded! Checking stream status.") + try: + self.container = self.client.containers.get('YouTubeLive') + self._logger.info("%s is streaming " % self.container.name) + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=True)) + except Exception, e: + self._logger.error(str(e)) + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=False)) ##~~ TemplatePlugin def get_template_configs(self): @@ -22,10 +36,47 @@ def get_assets(self): js=["js/youtubelive.js"], css=["css/youtubelive.css"] ) - + ##~~ SettingsPlugin def get_settings_defaults(self): - return dict(channel_id="") + return dict(channel_id="",stream_id="",streaming=False) + + ##~~ SimpleApiPlugin mixin + + def get_api_commands(self): + return dict(startStream=[],stopStream=[],checkStream=[]) + + def on_api_command(self, command, data): + if not user_permission.can(): + from flask import make_response + return make_response("Insufficient rights", 403) + + if command == 'startStream': + self._logger.info("Start stream command received for stream: %s" % self._settings.get(["stream_id"])) + if not self.container: + try: + self.container = self.client.containers.run("octoprint/youtubelive:latest",command=[self._settings.global_get(["webcam","stream"]),"pbea-b3pr-8513-40mh"],detach=True,privileged=True,name="YouTubeLive",auto_remove=True) + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=True)) + except Exception, e: + self._plugin_manager.send_plugin_message(self._identifier, dict(error=str(e),status=True,streaming=False)) + return + if command == 'stopStream': + self._logger.info("Stop stream command received.") + if self.container: + try: + self.container.stop() + self.container = None + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=False)) + except Exception, e: + self._plugin_manager.send_plugin_message(self._identifier, dict(error=str(e),status=True,streaming=False)) + else: + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=False)) + if command == 'checkStream': + self._logger.info("Checking stream status.") + if self.container: + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=True)) + else: + self._plugin_manager.send_plugin_message(self._identifier, dict(status=True,streaming=False)) ##~~ Softwareupdate hook def get_update_information(self): diff --git a/octoprint_youtubelive/static/js/youtubelive.js b/octoprint_youtubelive/static/js/youtubelive.js index c827fe2..7d7eb64 100644 --- a/octoprint_youtubelive/static/js/youtubelive.js +++ b/octoprint_youtubelive/static/js/youtubelive.js @@ -4,17 +4,109 @@ $(function () { self.settingsViewModel = parameters[0]; self.channel_id = ko.observable(); + self.stream_id = ko.observable(); + self.streaming = ko.observable(); + self.processing = ko.observable(false); + self.icon = ko.pureComputed(function() { + var icons = []; + if (self.streaming() && !self.processing()) { + icons.push('icon-stop'); + } + + if (!self.streaming() && !self.processing()){ + icons.push('icon-play'); + } + + if (self.processing()) { + icons.push('icon-spin icon-spinner'); + } + + return icons.join(' '); + }); + self.btnclass = ko.pureComputed(function() { + return self.streaming() ? 'btn-primary' : 'btn-danger'; + }); + // This will get called before the youtubeliveViewModel gets bound to the DOM, but after its depedencies have // already been initialized. It is especially guaranteed that this method gets called _after_ the settings // have been retrieved from the OctoPrint backend and thus the SettingsViewModel been properly populated. self.onBefireBinding = function () { self.channel_id(self.settingsViewModel.settings.plugins.youtubelive.channel_id()); + self.stream_id(self.settingsViewModel.settings.plugins.youtubelive.stream_id()); }; self.onEventSettingsUpdated = function (payload) { - self.channel_id = self.settingsViewModel.settings.plugins.youtubelive.channel_id(); + self.channel_id(self.settingsViewModel.settings.plugins.youtubelive.channel_id()); + self.stream_id(self.settingsViewModel.settings.plugins.youtubelive.stream_id()); }; + + self.onAfterBinding = function() { + $.ajax({ + url: API_BASEURL + "plugin/youtubelive", + type: "POST", + dataType: "json", + data: JSON.stringify({ + command: "checkStream" + }), + contentType: "application/json; charset=UTF-8" + }) + } + + self.onDataUpdaterPluginMessage = function(plugin, data) { + if (plugin != "youtubelive") { + return; + } + + if(data.error) { + new PNotify({ + title: 'YouTube Live Error', + text: data.error, + type: 'error', + hide: false, + buttons: { + closer: true, + sticker: false + } + }); + } + + if(data.status) { + if(data.streaming == true) { + self.streaming(true); + } else { + self.streaming(false); + } + + } + + self.processing(false); + }; + + self.toggleStream = function() { + self.processing(true); + if (self.streaming()) { + $.ajax({ + url: API_BASEURL + "plugin/youtubelive", + type: "POST", + dataType: "json", + data: JSON.stringify({ + command: "stopStream" + }), + contentType: "application/json; charset=UTF-8" + }) + } else { + $.ajax({ + url: API_BASEURL + "plugin/youtubelive", + type: "POST", + dataType: "json", + data: JSON.stringify({ + command: "startStream" + }), + contentType: "application/json; charset=UTF-8" + }) + } + } } // This is how our plugin registers itself with the application, by adding some configuration information to diff --git a/octoprint_youtubelive/templates/youtubelive_settings.jinja2 b/octoprint_youtubelive/templates/youtubelive_settings.jinja2 index 47835af..f003f0c 100644 --- a/octoprint_youtubelive/templates/youtubelive_settings.jinja2 +++ b/octoprint_youtubelive/templates/youtubelive_settings.jinja2 @@ -6,4 +6,10 @@ +
+ +
+ +
+
\ No newline at end of file diff --git a/octoprint_youtubelive/templates/youtubelive_tab.jinja2 b/octoprint_youtubelive/templates/youtubelive_tab.jinja2 index 7033537..f1bf04e 100644 --- a/octoprint_youtubelive/templates/youtubelive_tab.jinja2 +++ b/octoprint_youtubelive/templates/youtubelive_tab.jinja2 @@ -1,3 +1,6 @@
+
+ +
diff --git a/settings_screenshot.jpg b/settings_screenshot.jpg index 0a7b44c..a5b5f3c 100644 Binary files a/settings_screenshot.jpg and b/settings_screenshot.jpg differ diff --git a/setup.py b/setup.py index 60dcc53..3cc72f9 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "YouTube Live" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.1.0" +plugin_version = "0.2.0" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module @@ -33,7 +33,7 @@ plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = [] +plugin_requires = ["docker==2.7.0"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point