{"id":972,"date":"2014-08-21T13:29:17","date_gmt":"2014-08-21T17:29:17","guid":{"rendered":"http:\/\/blog.agilityfeat.com\/?p=972"},"modified":"2020-11-10T02:45:52","modified_gmt":"2020-11-10T02:45:52","slug":"pubsub-example","status":"publish","type":"post","link":"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/","title":{"rendered":"Using publish\/subscribe to synchronize presentation slides"},"content":{"rendered":"<p><a href=\"http:\/\/www.realtimeweb.co\/\" target=\"_blank\" rel=\"noopener noreferrer\"><img loading=\"lazy\" border=\"0\" src=\"\/wp-content\/uploads\/2014\/02\/book-300x300.png\" width=\"300\" height=\"300\" class=\"alignleft\" \/><\/a>A common use case for real-time messaging between web clients is to keep something in synch.  For example, in the Sensei tool that we built for one of our clients, each remote participant in an agile retrospective should be able to see the votes and comments made by other remote participants, and a moderator needs to be able to control the flow the of the meeting and advance the entire distributed group through each step of a process at the same time.<\/p>\n<p>Another example that we&#8217;ve built out is for a webinar tool.  We built a simple webinar tool that incorporates video and audio chat using WebRTC, and made that the basis of an e-book we&#8217;ve written for <strong><a href=\"http:\/\/www.realtimeweb.co\/\" target=\"_blank\" rel=\"noopener noreferrer\">\u00abBuilding Real-Time Web Applications using Node.js, Pub\/Sub networks, and WebRTC.\u00bb<\/a><\/strong><\/p>\n<p>One of the features of a webinar tool is that when the Presenter changes the slide being presented, that same slide (in this case an image) needs to be displayed in the browser of all the participants.<\/p>\n<p>The following is a sample chapter from our e-book that walks you through the code for how we keep the slides in synch in this webinar tool.  You can <strong><a href=\"https:\/\/github.com\/agilityfeat\/webinar-tool\" target=\"_blank\" rel=\"noopener noreferrer\">see the complete project available freely on GitHub<\/a><\/strong>, and of course you can <b><a href=\"http:\/\/www.realtimeweb.co\/\" target=\"_blank\" rel=\"noopener noreferrer\">pick up a copy of the book<\/a><\/b> in order to get our full explanation of how to build your own real-time applications.<br \/>\n<br clear=\"left\"\/><\/p>\n<h2>What you&#8217;re building<\/h2>\n<p>The following video gives you a quick tour of what you&#8217;re going to build in this chapter.<\/p>\n<p><iframe src=\"\/\/player.vimeo.com\/video\/104189623\" width=\"500\" height=\"237\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen><\/iframe> <\/p>\n<p><a href=\"http:\/\/vimeo.com\/104189623\">SynchronizedSlidesExample<\/a> from <a href=\"http:\/\/vimeo.com\/user24916355\">Arin Sime<\/a> on <a href=\"https:\/\/vimeo.com\">Vimeo<\/a>.<\/p>\n<p><br clear=\"left\"\/><\/p>\n<h1>Chapter 8: Coding Synchronized Slides<\/h1>\n<h2>Code Samples<\/h2>\n<p><em>Throughout our book, each chapter has code prepped if you want to start with that chapter and follow along, or if you want to see an example of our completed code at the end of the chapter.  You certainly don&#8217;t have to do it the way we did, but it&#8217;s a helpful guide if you&#8217;re having any problems.  By the way, this is a node application, so you&#8217;ll need to already have node installed and some familiarity with it to jump into this code.  If you&#8217;re not familiar with Node, our book does provide a brief intro to Node and Express so you can still use the book without prior experience.  Assuming you have node installed, then you can start the app by running \u00abnode server\u00bb in the root directory.<\/em><\/p>\n<p><strong>Prerequisite code for this chapter:<\/strong><\/p>\n<p><strong><a href=\"https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/designsetup\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/designsetup<\/a><\/strong><\/p>\n<p><strong>Completed code for this chapter:<\/strong><\/p>\n<p><strong><a href=\"https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup<\/a><\/strong><\/p>\n<h2>Introduction<\/h2>\n<p>In this chapter we are going to focus on synchronizing the slides between presenters and attendees.  This means that when the presenter presses the Next or Previous buttons, or the number of a particular slide they want to display, then that slide should change on the presenter\u2019s screen and all attendees\u2019 screens using real-time messaging.<\/p>\n<p>To see the full code for this chapter, check out this branch of the webinar-tool application:<\/p>\n<p><strong><a href=\"https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup<\/a><\/strong><\/p>\n<h2>Setting up the Presenter \u201clogin\u201d<\/h2>\n<p>The first thing that we need to do is determine if a webinar user is the presenter or an attendee.  Only presenters will have the power to change slides.  Only attendees have the power to post comments.<\/p>\n<p>If you\u2019re going to take the code from this book and make a proper webinar tool, then you should put in some real security with actual authentication.  But I\u2019ve always been a big fan of not bothering with the mundane things like logins until you have to, because logins are not that valuable by themselves.<\/p>\n<p>Since this book is not about user authentication models and I don\u2019t want to waste your time with that, we\u2019re going to do a cheap trick to determine if someone is a presenter or not.<\/p>\n<p>After this chapter, if you are an attendee, you access the application locally this way:<\/p>\n<p><strong><a href=\"http:\/\/localhost:3000\/\" target=\"_blank\" rel=\"noopener noreferrer\">http:\/\/localhost:3000\/<\/a><\/strong><\/p>\n<p>If you are a presenter, you should access it this way:<\/p>\n<p><strong><a href=\"http:\/\/localhost:3000\/presenter\" target=\"_blank\" rel=\"noopener noreferrer\">http:\/\/localhost:3000\/presenter<\/a><\/strong><\/p>\n<p>That\u2019s it!  It\u2019s not exactly secure, but it\u2019s good enough for our learning purposes in this book.  We\u2019re just setting this up in the routes for our application:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/e781d0a936e07e7b3b34.js\"><\/script><\/p>\n<p>In the <strong>routes\/routes.ejs<\/strong> file, we\u2019ve just added an additional route for \u2018\/presenter\u2019 underneath the root URL route.  Both will render the <strong>index.ejs<\/strong> file.<\/p>\n<p>Then in our <strong>webrtc.agility.js<\/strong> file (<em>this is the primary javascript file we&#8217;re putting the code for our book in<\/em>), we\u2019ve added a variable \u201cisPresenter\u201d to the agility_webrtc definition, and then in the init() method:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/2a4402217cdaa6635a04.js\"><\/script><\/p>\n<p>Now we can access the <strong>isPresenter<\/strong> variable anywhere in the layout to determine which view we should show the current user, and what functionality they can use.<\/p>\n<h2>Changing the responsive layout based on Presenter\/Attendee view<\/h2>\n<p>If you recall what the webinar tool looked like after the design chapter, you were seeing only the presenter\u2019s view.  The next\/previous buttons should not be visible to attendees, and the circle icons for each slide should only have a click event for the presenter.  In the <strong>templates.html<\/strong> file, this is handled just by checking that isPresenter variable:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/dfec36b23567524ef92f.js\"><\/script><\/p>\n<p>In addition to the slide controls, we should also go ahead and change the rest of the template for the upcoming chapters where we are dealing with attendee voting and comments.  Those controls should only be visible to attendees, not to presenters.  That is also handled in the <strong>templates.html<\/strong> file with our <strong>isPresenter<\/strong> variable:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/96e97a46e73e4bdaae86.js\"><\/script><\/p>\n<p>This is what the Attendee\u2019s view should look like now:<\/p>\n<p><a href=\"\/wp-content\/uploads\/2014\/08\/SlideSynchAttendeeView.png\"><img loading=\"lazy\" src=\"\/wp-content\/uploads\/2014\/08\/SlideSynchAttendeeView-1024x781.png\" alt=\"SlideSynchAttendeeView\" width=\"510\" height=\"388\" class=\"size-large wp-image-977\" style=\"max-width: 510px; max-height: 388px;\" \/><\/a><br clear=\"left\"\/><br \/>\n<i>Attendee view after handling for isPresenter<\/i><\/p>\n<p>Note that the \u201cRate Current Slide\u201d and comment box controls are visible to the attendees, but the Previous\/Next buttons are now hidden.<\/p>\n<p>For the presenter, the bottom of the screen should now look like:<\/p>\n<p><a href=\"\/wp-content\/uploads\/2014\/08\/SlideSynchPresenterView.png\"><img loading=\"lazy\" src=\"\/wp-content\/uploads\/2014\/08\/SlideSynchPresenterView-1024x388.png\" alt=\"SlideSynchPresenterView\" width=\"510\" height=\"193\" style=\"max-width: 510px; max-height: 193px;\" class=\"size-large wp-image-978\" \/><\/a><br clear=\"left\"\/><br \/>\n<i>Bottom part of the Presenter&#8217;s view after isPresenter variable implemented<\/i><\/p>\n<p>Note that the Previous\/Next buttons are back, and along the bottom we have some placeholder html for the eventual voting result displays.<\/p>\n<h2>Changing slides<\/h2>\n<p>Now that the controls are all placed on the page properly for our presenter, it\u2019s time to hook them up to some code to change the slides.  In our <strong>webrtc.agility.js<\/strong> file, we have added two functions to a <strong>setBinds<\/strong> method.<\/p>\n<p>The first method will bind a function to the <strong>\u201conclick\u201d<\/strong> event of any control with the class <strong>\u201c.control\u201d<\/strong> attached to it.  If you look closely at the layout from our <strong>templates.html<\/strong> file above, you\u2019ll see that only the Previous and Next buttons have the css class control attached to them:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/128ba054db6fa0a747d3.js\"><\/script><\/p>\n<p>Here is the first method for binding the Previous and Next buttons:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/968f2d0d6829f34ece21.js\"><\/script><\/p>\n<p>Notice that this function sets an <strong>is_next<\/strong> variable based on whether or not the button that generated the <strong>\u201conclick\u201d<\/strong> event also has the <strong>\u201cnextSlide\u00bb<\/strong> class attached to it.  In this way we can use one event handler for both buttons and just determine the command based on attach CSS classes.  If you wanted to add more buttons to the UI for some reason, this allows you to do that only in the templates file without additional code bindings needed (although we wouldn\u2019t recommend filling the screen with extra controls!).<\/p>\n<p>The rest of the method just determines the right number for the next slide, and then sets the image carousel to use that number slide.<\/p>\n<p>There is one more function we want to add to the <strong>setBinds<\/strong> method.  In addition to the Previous and Next buttons, we have also put a set of numbered circles on the layout inbetween the two buttons.  Each numbered circle represents a slide in the presentation deck (more on where those are stored later).<\/p>\n<p>To make those circles bind to a function for navigating directly to that slide, we also add this function to the <strong>setBinds<\/strong> method:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/63c3d4564523e4bad69c.js\"><\/script><\/p>\n<p>The code is simpler in this case because we know exactly what number slide we want to navigate to.<\/p>\n<h2>Synchronizing the slides across clients<\/h2>\n<p>There\u2019s one last thing we haven\u2019t explained yet about the code above, but you probably already noticed it.  When you hit any of the slide navigation controls, the function ends with a method like this:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/70f7040adefcf64d7c34.js\"><\/script><\/p>\n<p>In either function, we are going to publish a message to our channel with a type of <strong>\u201cSLIDE\u201d<\/strong> to indicate a slide change.  We have stored the slide number that we want everyone to switch to in a variable named <strong>\u201cslide_to\u201d<\/strong>, and we\u2019ll pass that value on the channel as well.<\/p>\n<p>Back up in the <strong>init()<\/strong> function of our <strong>webrtc.agility.js<\/strong> file, we\u2019ve added a statement to subscribe to the pubnub channel:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/531fb38df37e5da6d8ec.js\"><\/script><\/p>\n<p>Now whenever a message is published to the channel, everyone subscribed will receive a callback to the onChannelListMessage method.  This callback method is defined in our agility_webrtc variable as:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/79c8205c1571e97f75ee.js\"><\/script><\/p>\n<p>We\u2019ve setup this method to use a switch statement, where each case will be one of the possible message.type\u2019s that we implement in our application.  For now, we are only implementing <strong>\u201cSLIDE\u201d<\/strong> in order to change the slides.  If a message with any other type is published, it will be ignored.<\/p>\n<p>That <strong>SLIDE<\/strong> message will then call a method <strong>changeSlide(slide_number)<\/strong>, which we have implemented right above the <strong>onChannelListMessage()<\/strong> in our code.<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/7f792c4822324b7d73e8.js\"><\/script><\/p>\n<p>This final method for changing the slides is pretty straightforward.  The options value that you pass in is the slide number to change to, and so the carousel holding the presentation images is set to switch to that slide number.  We\u2019ll go to slide number one if no options are passed in.<\/p>\n<p>We\u2019ll also change the value of an <strong>active_index<\/strong> variable, and use that at the bottom to add a CSS class called <strong>\u201cactive\u201d<\/strong> to the appropriate circled number underneath the slides, so that we can show which one is the currently selected slide.<\/p>\n<h2>Testing the slide synchronization<\/h2>\n<p>At this point, your code to change the slides is complete.  If you run <strong>\u201cnode server\u201d<\/strong> and navigate back to the application at <strong>localhost:3000<\/strong> in two different browsers.  Use the <strong><a href=\"http:\/\/localhost:3000\/presenter\/\" target=\"_blank\" rel=\"noopener noreferrer\">http:\/\/localhost:3000\/presenter\/<\/a><\/strong>  URL in one browser, and in the other, just use the base URL <strong><a href=\"http:\/\/localhost:3000\/\" target=\"_blank\" rel=\"noopener noreferrer\">http:\/\/localhost:3000\/<\/a><\/strong>.  If you are opening both in a Chrome browser, then open one URL in \u00abIncognito Mode\u00bb so that you have two distinct sessions.<\/p>\n<p>In the presenter view, you should see the navigation controls.  Press any of the controls and you should see the slides change in the presenter\u2019s browser.  We have put three simple placeholder slides in the application, each with the same number in the slide graphic so you can easily see the changes.<\/p>\n<p>As the presenter is changing slides, you should also see the same slide changed in the regular attendee browser window, with only a momentary delay.  Notice again that the attendee has no navigation controls, and that the circled numbers showing which slide you are on are not clickable for attendees.<\/p>\n<p>If you\u2019re having any trouble getting this to work, you\u2019ll want to fix that before moving on to subsequent chapters.  <strong>These are the problem areas to look out for:<\/strong><\/p>\n<p><strong>Browser type<\/strong> \u2013 Although PubNub supports any recent modern browser, our code is optimized for Chrome and Firefox.  Try one of those if you have problems with other browsers.<\/p>\n<p><strong>PubNub keys<\/strong> \u2013 Remember that you need to update our sample code with your own publish and subscribe keys from PubNub.  The variables containing those keys are located near the top of the webrtc.agility.js file.  Just search on \u201cyour pub key\u201d and you\u2019ll find the credentials declaration.  Remember that anytime you refresh your code from one of our branches in this project, you\u2019ll need to put your keys back in so keep them handy.<\/p>\n<p><strong>Javascript errors<\/strong> \u2013 If you\u2019re still having trouble, open the Javascript console in your browser (In Chrome, this is accessed by <strong>View->Developer->Javascript Console<\/strong>).  You need to have a working internet connections, or you will see errors loading the pubnub.js file, and that the PUBNUB variable is not declared.<\/p>\n<p>You can get a copy of our code for this chapter on the following branch, it\u2019s always a good idea to compare your implementation to that or to just use our code as a base for future chapters:<br \/>\n<strong><a href=\"https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/github.com\/agilityfeat\/webinar-tool\/tree\/syncsetup<\/a><\/strong><\/p>\n<p>There\u2019s one last fun thing I recommend you do now.  Remember how the presenter on a mobile device has their own special layout with Previous and Next buttons?  If you have your code deployed on a publicly accessible URL or IP Address, then you can bring up the presenter\u2019s view on your iPhone.  Pressing the Previous and Next buttons from your phone will now send the same slide synchronization messages to all connected users.<\/p>\n<p><a href=\"\/wp-content\/uploads\/2014\/08\/SynchSlides_PresenterMobileView.png\"><img loading=\"lazy\" src=\"\/wp-content\/uploads\/2014\/08\/SynchSlides_PresenterMobileView-735x1024.png\" alt=\"SynchSlides_PresenterMobileView\" width=\"510\" height=\"710\" style=\"max-width: 510px; max-height:710px;\" class=\"alignnone size-large wp-image-979\" \/><\/a><br clear=\"left\"\/><br \/>\n<i>Presenter&#8217;s mobile view with navigation controls<\/i><\/p>\n<p>I like to do that when using this tool at conferences.  From my laptop, I bring up the presenter view and project it on a big screen.  Attendees at the talk are looking at the slides from their mobile devices on an attendee view.  I then use my iPhone from the Presenter\u2019s URL and I can show them how pressing the Next button on my phone will advance the slide on the big screen and on their mobile devices.  It\u2019s a cheap way to get ooh\u2019s and ahh\u2019s!<\/p>\n<h2>Adding your own slides to the tool<\/h2>\n<p>It\u2019s all working now, right!  Excellent.  But those three boring slides we put in our only good for testing the application.  They don\u2019t make for a very good webinar.<\/p>\n<p>We\u2019ve kept this application intentionally simple, and the presentation is just a series of numbered images stored in the <strong>public\/images\/presentation<\/strong> folder.  If you want to change the slides, just update or add to those images.<\/p>\n<p><img loading=\"lazy\" src=\"\/wp-content\/uploads\/2014\/08\/SlideSynchPresentationImages.png\" alt=\"SlideSynchPresentationImages\" width=\"342\" height=\"272\" style=\"max-width: 342px; max-height=272px;\" class=\"alignnone size-full wp-image-980\" \/><br clear=\"left\"\/><\/p>\n<p>After putting in the appropriate image files, you also need to make the application aware of them.  Look near the top of the <strong>webrtc.agility.js<\/strong> file for this declaration of the slide_pics:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/asime\/76af1aac908bebd8c3dd.js\"><\/script><\/p>\n<p>Just add\/remove images to the slide_pics and you will be all set!  We\u2019re using png\u2019s that resize well, so you if you\u2019re going to use this webinar tool for anything more than fun, make sure you pay attention to how the presentation images you use look on browsers, mobile devices, and maybe even projectors.<\/p>\n<p>That\u2019s it for synchronizing slides!  If you\u2019ve got some time on your hands and want to enhance what we\u2019ve done in this chapter, consider allowing presenters to upload their own images, change the order of them, or allow the display of powerpoint files natively.<\/p>\n<h2>Pick up your own copy of this e-book<\/h2>\n<p>Now that you know how to do the basics of synchronizing the slides in our webinar tool, you probably have everything you need to build a simple real-time messaging application.  If you interested in seeing more examples like this, or you want to go the next step and learn how we incorporated video and audio chat into our webinar tool, then make sure to check out our e-book <strong><a href=\"http:\/\/www.realtimeweb.co\/\" target=\"_blank\" rel=\"noopener noreferrer\">\u00abBuilding Real-Time Web Applications using Node.js, Pub\/Sub networks, and WebRTC.\u00bb<\/a><\/strong>  And to keep up with the best posts about real-time technologies across the web, please join us every week at <strong><a href=\"http:\/\/www.realtimeweekly.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">RealTimeWeekly.com<\/a><\/strong>.<\/p>","protected":false},"excerpt":{"rendered":"<p>A common use case for real-time messaging between web clients is to keep something in synch. For example, in the Sensei tool that we built for one of our clients, each remote participant in an agile retrospective should be able to see the votes and comments made by other remote participants, and a moderator needs [&hellip;]<\/p>","protected":false},"author":4,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":""},"categories":[116],"tags":[],"jetpack_featured_media_url":"","yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v15.7 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Using publish\/subscribe to synchronize presentation slides - AgilityFeat Panama Software Test Center<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Using publish\/subscribe to synchronize presentation slides - AgilityFeat Panama Software Test Center\" \/>\n<meta property=\"og:description\" content=\"A common use case for real-time messaging between web clients is to keep something in synch. For example, in the Sensei tool that we built for one of our clients, each remote participant in an agile retrospective should be able to see the votes and comments made by other remote participants, and a moderator needs [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/\" \/>\n<meta property=\"og:site_name\" content=\"AgilityFeat Panama Software Test Center\" \/>\n<meta property=\"article:published_time\" content=\"2014-08-21T17:29:17+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2020-11-10T02:45:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/agilityfeatpanama.com\/wp-content\/uploads\/2014\/02\/book-300x300.png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\">\n\t<meta name=\"twitter:data1\" content=\"13 minutes\">\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebSite\",\"@id\":\"https:\/\/34.200.113.64\/#website\",\"url\":\"https:\/\/34.200.113.64\/\",\"name\":\"AgilityFeat Panama Software Test Center\",\"description\":\"AgilityFeat Panama offers customized, multilevel web and mobile software testing for a variety of industries.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":\"https:\/\/34.200.113.64\/?s={search_term_string}\",\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/#primaryimage\",\"inLanguage\":\"en-US\",\"url\":\"\/wp-content\/uploads\/2014\/02\/book-300x300.png\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/#webpage\",\"url\":\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/\",\"name\":\"Using publish\/subscribe to synchronize presentation slides - AgilityFeat Panama Software Test Center\",\"isPartOf\":{\"@id\":\"https:\/\/34.200.113.64\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/#primaryimage\"},\"datePublished\":\"2014-08-21T17:29:17+00:00\",\"dateModified\":\"2020-11-10T02:45:52+00:00\",\"author\":{\"@id\":\"https:\/\/34.200.113.64\/#\/schema\/person\/c8d60d597071526db386b2b8a4afac64\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/agilityfeatpanama.com\/en\/blog\/2014\/08\/pubsub-example\/\"]}]},{\"@type\":\"Person\",\"@id\":\"https:\/\/34.200.113.64\/#\/schema\/person\/c8d60d597071526db386b2b8a4afac64\",\"name\":\"arin\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/34.200.113.64\/#personlogo\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/cc498e210512c707ed769986dd745896?s=96&d=mm&r=g\",\"caption\":\"arin\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","_links":{"self":[{"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/posts\/972"}],"collection":[{"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/comments?post=972"}],"version-history":[{"count":1,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/posts\/972\/revisions"}],"predecessor-version":[{"id":1345,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/posts\/972\/revisions\/1345"}],"wp:attachment":[{"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/media?parent=972"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/categories?post=972"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/agilityfeatpanama.com\/en\/wp-json\/wp\/v2\/tags?post=972"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}