Building a YouTube LTI app

If you didn't know, I'm a fan of LTI. What is LTI? It's a cross-platform standard in edtech for what essentially amounts to Facebook apps for learning. Like Bejeweled Blitz running within Facebook, you now have Quizlet flashcards running with a course web site. It's pretty slick, and it makes it a lot easier to build apps that work in different learning platforms.

LTI was originally called BLTI, which always made me hungry.

Anyway. I thought I'd start sharing some examples of LTI apps and how they could be built. I'll start with an app to embed YouTube videos within course content. The server code is ruby-esque pseudocode which hopefully is grok-able. I've included an actual ruby app at the end as well for reference.

For this example I'll use the standard LTI workflow (Canvas has what I think is a cleaner workflow, but it's not officially supported right now. I'll cover that in another post). With LTI, the teacher or administrator sets up an LTI app. They specify a launch URL, a consumer key and a shared secret. Then when anyone clicks a link to access the app, it will be loaded in an iframe within the platform. In our case, the teacher would add one or more YouTube App placeholders to their course content. Then the teacher would go to each of these placeholders and specify which YouTube video to associate with each placement. Students who launch the app after it is set up will just see the selected YouTube video.




App "launches" happen when a user accesses the app from its placement within the learning platform. A launch consists of a client-side form post with an iframe or new tab as the form target. A bunch of data gets sent as POST parameters, including an OAuth-style signature derived from the parameters, key and secret. For simplicity (and since we're just dealing with public videos and don't care if the user is actually authenticated) we're going to ignore signature verification this time around. In a production environment I'd add the check or someone could potentially stuff your database with garbage data.

# Handle POST requests to the endpoint "/lti_launch"
post "/lti_launch" do
  # do magical YouTube-y stuff
end

The only values we're going to worry about are resource_link_id and tool_consumer_instance_guid. The combination of these values should give us a globally unique identifier that we can use to remember this "launch". We'll call this the placement_id, and since any launch from the same placement will result in the same placement_id, we can use it to remember which video the teacher picked for this placement. If the teacher adds multiple YouTube App placeholders to their course, each placeholder will launch with a different resource_link_id (tool_consumer_instance_guid is an extra precaution in case platforms use the same link id) and result in a different placement_id.

# Handle POST requests to the endpoint "/lti_launch"
post "/lti_launch" do
  placement_id = params['resource_link_id'] + 
      params['tool_consumer_instance_guid']
  placement = find_placement(placement_id)
  if placement
    redirect_to "https://youtube.com/embed/"  + placement.video_id
  else
    # use a cookie-based session to remember placement permission
    session["can_set_" + placement_id] = true

    # let the user pick the video to use for this placement
    redirect_to ("/youtube_search.html?placement_id=" + placement_id)
  end
end

Now we need to build the YouTube video selection page. I like jQuery, and YouTube supports jsonp, so the querying part is actually pretty easy. For the sake of simplicity we're going to build an "I'm feeling lucky" YouTube searcher that just assumes the first result is the one you want.

<html>
  <body>
  <h1>YouTube Searcher</h1>
  <p>Search for a YouTube video below. The first result
  is what will be shown whenever anyone loads this app.
  </p>
  <form id="form">
    <input type="text" name="query" id="query"/>
    <button type="submit">I'm Feeling Lucky</button>
  </form>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
  <script>
    // Remember the placement_id so we can persist one a video is found
    var placement_id = location.href.match(/placement_id=([^&#]+)/)[1];
    
    // AJAX call to remember the placement for future launches
    function rememberVideo(video_id) {
      $.ajax({
        url: "/set_video",
        type: 'POST',
        data: {
          placement_id: placement_id,
          video_id: video_id
        },
        success: function(data) {
          if(data.success) {
            alert("Video set! Redirecting now...");
            location.href = "https://www.youtube.com/embed/" + video_id;
          } else {
            alert("There was a problem setting the video.");
          }
        },
        dataType: 'json'
      });
    }
    
    // JSONP call to search for YouTube videos
    function findVideo(query) {
      // YouTube API endpoint.
      var url = "https://gdata.youtube.com/feeds/api/videos?v=2&q=";
      url = url + query + "&orderby=relevance&alt=json-in-script";
      $.ajax({
        url: url,
        success: function(data) {
          // Grad the *first* video and use that one.
          // If you wanted to list the videos instead this would be the place to
          // iterate through the results. The attribute names are... different, 
          // but you can find thumbnails, descriptions, durations, etc.
          var video_id = data.feed.entry[0].id['$t'].match(/\w+$/)[0];
          rememberVideo(video_id);
        },
        dataType: 'jsonp'
      });
    }
    
    // When the user submits the search form, trigger the JSONP lookup
    $("#form").submit(function(event) {
      event.preventDefault();
      findVideo($("#query").val());
    });
  </script>
  </body>
</html>

We also need a server-side piece to persist the placement_id-to-video mapping to the db.

# Handle POST requests to the endpoint "/set_video"
post "/set_video" do
  if session["can_set_" + params['placement_id']]
    create_placement(params['placement_id'], params['video_id'])
    return '{"success": true}'
  else
    return '{"success": false}'
  end
end

Now the first person to launch the YouTube app after it's been placed will see the search interface that will let them specify which video to show, and anyone who launches the app after that will get redirected to the selected video.

Sinatra app source code available here.

Comments

dvs said…
Hi Brian, hope you still get notified of these comments - I'm trying to built an LTI provider and am having a lot of trouble understanding best standards for key generation and exchange. Am I correct in thinking the LTI provider should generate the key and secret for each individual resource?
Brian Whitmer said…
Hi dvs, the LTI provider (you) are responsible to create a key and secret. How unique these keys and secrets are is up to you, but it's best practice to generate a new key and secret for each admin/teacher. It doesn't have to be for each resource (and would be a bit annoying if the teacher needed a new key each time they added another resource), you just want to make sure if something goes wrong you have unique enough keys that you can close off a security breach without having to impact too many people. Make sense?
Karthick said…
Hi Brain - I have a clarification regarding LTI. Can I make my custom web application to act as LTI consumer. Meaning, I would like my custom application (not LMS) to accept request from tool provider and launch that LTI module inside my custom application.

Regards,
SAMANA said…
I have a clarification regarding LTI. Can I make my custom web application to act as LTI consumer. Meaning, I would like my custom application (not LMS) to accept request from tool provider(for youtube specifically) and launch that LTI module inside my custom application. Also let me know who are providers of youtube.
Brian Whitmer said…
Hello! Any web application can be an LTI tool consumer. You can see the list of certified tool consumers here (http://developers.imsglobal.org/catalog.html). Not all of the apps listed are LMSs, and in some ways LTI can work well as just an integration point for generic content management solutions.
Brian Whitmer said…
Hi Samana, see the reply above. The YouTube LTI app was written by me as a side project, but the code is open source on github as part of a larger repository of LTI apps here (https://github.com/whitmer/edu_apps).
Unknown said…
Brian, great post on LTI. I'm using Ayamel (external video app tool) to embed videos into Canvas. Ayamel will give me the analytics for each specific user if they are logged in before they watch the embedded videos. They can be logged in or authenticated with LTI, but they have to click on a link to load an iframe or load Ayamel in a separate tab in order to be authenticated. It doesn't really make sense to make them do that every time they log into Canvas. Is there a way to authenticate a student once at the beginning of the semester and then have them automatically authenticate after that? If it's possible then we can have videos embedded in Canvas using Ayamel, which will track user specific data on videos at the activity level. Any ideas?
There has been a change in the YouTube API, so there are some changes needed in youtube_search.html:

function findVideo(query) {
// YouTube API endpoint.


var url = "https://www.googleapis.com/youtube/v3/search?&q=" + query + "&order=relevance&part=snippet";
Karthika Shree said…
It's interesting that many of the bloggers to helped clarify a few things for me as well as giving.Most of ideas can be nice content.The people to give them a good shake to get your point and across the command.
Java Training in Chennai
Herry Johnson said…
Appzsearch.co The New and easy way to Search App Store - iTunes App & Apk here. A best App Search Engine Just visit the site and search for any App or Apk – AppzSearch.
Unknown said…
I am having this problem quite frequently, sometimes when I have the exact name of the video and the author. Most of the time searching by url also not working for me. Searching for a post to get help.
Emma Gamer Girl
alex said…
I am having troubles running your repository https://github.com/whitmer/edu_apps. The rackup command fails with uninitialized constant Sinatra::Main (NameError). Am i missing something?
Thanks
Unknown said…
Hi Brian,
I'm new to Canvas. Recently I'm created small website it contains userid, user progress and all user data. I'm done frontend application now i want to access that canvas data. I'm added website in canvas through LTI app. just tell me how to access username and user details using LTI variable. please give sample example

Thanks
Mrbk30 said…

Very Informative blog thank you for sharing. Keep sharing.

Best software training institute in Chennai. Make your career development the best by learning software courses.

devops training in chennai
rpa training in chennai
cloud computing training in chennai
Unknown said…
Infycle Technologies, the best software training institute in Chennai Perfect place to learn Big Data Hadoop Training in Chennai for experienced, and Tech professionals. And we also put up other courses like Data Science, Manual and Automation Testing, DevOps, Medical Coding, Oracle, Java, Data Science, AWS, Python, etc., at the end of the course period the pupil will be able to crack jobs on top MNC’s. for more details call 7504633633.
Unknown said…
Those guidelines additionally worked to become a good way to
recognize that other people online have the identical fervor like mine
to grasp great deal more around this condition.
dot net training institute in chennai
core java course in chennai
Manual Testing Training institute in Chennai

Popular Posts