Sample: Create a mail app to view YouTube videos in Outlook

apps for Office

This sample mail app lets users view a YouTube video from a message or appointment item that contains the video URL, without leaving Outlook 2013.

Last modified: February 17, 2014

Applies to: Exchange Online | Exchange Server 2013 | Exchange Server 2013 SP1 | Outlook 2013 | Outlook 2013 RT | Outlook 2013 SP1 | Outlook Web App | OWA for Devices

   Office.js: v1.0

   Apps for Office manifests schema: v1.0

Note Note

"Outlook" in this article refers to the Outlook rich client, Outlook RT, Outlook Web App, and OWA for Devices.

In this topic, you will learn about the following features:

  • Using regular expressions to obtain URLs to YouTube videos in the body of the selected message or appointment.

  • Using a data API and web service to obtain metadata about a YouTube video.

  • Using a YouTube embedded player to play a YouTube video in the app pane.

A mail app is contextual and is activated when circumstances, with respect to the current item, satisfy the activation rules of the app. In this YouTube app example, Outlook activates the app if the body of the selected appointment, email message, meeting request, response or cancellation, in the Reading Pane or inspector, contains a YouTube video URL. When the user chooses the app button, the app pane opens and displays the first video in a YouTube embedded player, together with details about the video. The app pane also displays the thumbnails of any subsequent videos. The end user can choose a thumbnail to view any of the videos in the YouTube embedded player without leaving Outlook.

See the YouTube mail app in Outlook.

Video

Before trying this app the first time, follow the instructions in How to: Install sample mail apps in Outlook to use the Exchange Admin Center to install the app for use in Outlook. Then, create an email message or appointment, and add one or more URLs to YouTube videos in the body. When you display the message in the Reading Pane, or the message or appointment in an inspector, Outlook activates the app. You can then choose the YouTube app button. In the app pane, choose a video thumbnail to select a video, or the YouTube embedded player to play a video.

This section describes the XML manifest and HTML and JavaScript code for the YouTube app.

You can download the source code files for the YouTube mail app at Mail apps for Outlook: Create a mail app to view YouTube videos.

XML manifest

An XML manifest defines the following metadata for an app:

  • Identity.

  • Capability required of the host application

  • Form factors.

  • Corresponding HTML file for possibly each form factor.

  • Display requirements.

  • Necessary permissions.

  • Activation rules.

The manifest uses the OfficeApp element as the root element to enclose various child elements of the MailApp complex type in a sequential order. This section highlights a few areas of interest in the manifest that characterize the YouTube app.

  • The type of this app—The OfficeApp element in the following XML line specifies that this app is of the MailApp complex type, which extends the base complex type OfficeApp.

    <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:type="MailApp">
    
  • The identifier of the app—The Id element in the following XML line uniquely identifies and differentiates this app from other apps for Office.

    <Id>971E76EF-D73E-567F-ADAE-5A76B39052CF</Id>
    
  • The label on the app button—The DisplayName element in the following XML line specifies "YouTube" as the label. Outlook displays the app button in the app bar if activation conditions, as specified in the manifest, are satisfied by the circumstances with respect to the current item.

    <DisplayName DefaultValue="YouTube"/>
    
  • A verbose description for the app — In the next set of XML lines, the Description element contains a DefaultValue attribute that contains a description of typically the purpose of the app. The DefaultValue attribute corresponds to the en-us locale specified by the DefaultLocale element. To support a description for the fr-fr locale, this app specifies an Override child element. For more information about supporting multiple locales, see Designing an app for Office for specific locales.

      <Description DefaultValue=
      "Watch YouTube videos referenced in the e-mails you receive without 
    leaving your email client.">
        <Override Locale="fr-fr" Value="Visualisez les vidéos YouTube 
    référencées dans vos courriers électronique directement depuis 
    Outlook et Outlook Web App."/>
      </Description>
    
  • The capabilities that this app requires of a host application—The Capabilities element includes one Capability element in the following XML lines. This means the app requires only one capability, the Mailbox capability. An application that supports the Mailbox capability can become a host application for this app. Note that the subsequent specification of the DesktopSettings, TabletSettings, or PhoneSettings in the manifest additionally restrict the host applications that can run on these form factors.

    <Capabilities>
      <Capability Name="Mailbox"/>
    </Capabilities>
    
  • The form factors supported by this app, the corresponding location of HTML file, and default display height, in pixels—The DesktopSettings element in the following XML lines specifies that this app supports the desktop form factor, and the TabletSettings element specifies its support to run on a tablet. The corresponding SourceLocation element for each form factor specifies that Outlook can obtain the youtube.htm file at http://webserver/youtube/youtube.htm. The corresponding RequestedHeight element specifies that Outlook should try to display an app pane that is 216 pixels high on either the desktop or tablet by default.

    Note Note

    If you are trying this YouTube app, you should replace the URL for the HTML file, http://webserver/youtube/youtube.htm, with the actual location of the HTML file in your configuration.

    <DesktopSettings>
      <!-- Change the following line to specify the web server -->
      <!-- that hosts the HTML file. -->
      <SourceLocation DefaultValue=
      "https://webserver/YouTube/YouTube.htm"/>
      <RequestedHeight>216</RequestedHeight>
    </DesktopSettings>
    <TabletSettings>
      <!-- Change the following line to specify the web server -->
      <!-- that hosts the HTML file. -->
      <SourceLocation DefaultValue=
      "https://webserver/YouTube/YouTube.htm"/>
      <RequestedHeight>216</RequestedHeight>
    </TabletSettings>
    
    
  • The permission requested by this app—The Permissions element in the following XML line specifies that this app needs the read item permission. As seen in the next bullet point, this app needs this permission to use regular expressions in its activation rules.

    <Permissions>ReadItem</Permissions>
    
  • The activation rule—This app specifies a rule of the ItemHasRegularExpressionMatch complex type, and specifies a regular expression, named VideoURL, that looks for a YouTube video URL in the body of the selected mail or appointment item.

    <Rule xsi:type="ItemHasRegularExpressionMatch" 
    PropertyName="BodyAsPlaintext" RegExName="VideoURL" RegExValue=
    "http://(((www\.)?youtube\.com/watch\?v=)|(youtu\.be/))[a-zA-Z0-9_-]{11}"/>
    
    

See Appendix A: XML manifest for a complete listing of the XML manifest.

HTML and JavaScript implementation

The YouTube sample app includes one HTML file that contains all the HTML and JavaScript code. This section highlights a few areas of interest in the HTML and JavaScript code for this app.

  • A document mode for Internet Explorer 9—Because Outlook uses Internet Explorer 9 components in a browser control to open the HTML page, you must place the following HTML line as a child element of the head element in your HTML file, before all other elements except for the title element and other meta elements.

    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    
  • Location of the JavaScript libraries and embedding JavaScript code—The HTML file specifies that the JavaScript API library file, Office.js, is to be accessed from the Content Delivery Network (CDN), and that the JavaScript library files, MicrosoftAjax.js and strings_en-us.js, are in the same folder as the HTML file. The HTML file also embeds JavaScript for this app, as shown in the following HTML lines.

        <script src="MicrosoftAjax.js" type="text/javascript"></script>
        <!-- Use the CDN reference to Office.js. -->
        <script src="https://appsforoffice.microsoft.com/lib/1.0/hosted/Office.js"
            type="text/javascript"></script>
        <script src="strings_en-us.js" type="text/javascript"></script>
        <script type="text/javascript">
    
    
  • Layout of the HTML page—This app uses the following HTML lines to initially create a single-row table with three columns for a video thumbnail, YouTube embedded video player, and video details. Since details can be variable length, the app uses overflow: auto to insert a scroll bar if necessary. Because a message that satisfies the activation rules can contain a variable number of YouTube URLs, the app uses JavaScript code to dynamically determine the actual number of rows, as well as the corresponding thumbnail and video details.

    <body style="margin: 8px">
        <table cellpadding="0px" cellspacing="0px" 
            style="table-layout: fixed" height="200px">
            <tr>
                <td id="thumbnails" valign="top" />
                <td id="embeddedVideo" valign="top" />
                <td valign="top">
                    <div id="details" style="margin-left: 10px; 
                     height: 200px; overflow: auto" />
                </td>
            </tr>
        </table>
    </body>
    
  • Defining the initialize function—Every app for Office must define an event handler for the initialize event, which occurs when the run-time environment is loaded. Upon the initialize event, the app is ready to interact with Outlook.

    Based on the current message or appointment as specified by the item property of the Mailbox object, this app uses the getRegExMatches method to get the YouTube URLs that match the regular expression named VideoURL that is specified in the XML manifest file. It then passes the array of matches to the main function of this app, init.

            // Define the initialize function, which handles the 
            // initialize event of this mail app.
            Office.initialize = function () {
                init(
                Office.context.mailbox.item.getRegExMatches().VideoURL);}
    
    
  • Defining the main function, init—This function takes as an input parameter an array of YouTube URLs and dynamically builds the HTML to display the corresponding thumbnail and details for each video. For each of the YouTube video URLs, this function initializes the corresponding structure in the global videos array, specifies the selectVideo function as the event handler for the onclick event, and calls the loadVideoDetails function to retrieve details from YouTube. In case there is only one URL in the item, the init function does not display any thumbnail. Finally, this function calls selectVideo(0) to set up the first video in the YouTube embedded player and displays its details.

            // Based on an array of YouTube URLs that match the regular 
            // expression, display the corresponding thumbnails and video 
            // details, and set up the first video in the YouTube 
            // embedded player.
            function init(videoURLs) {
                var html = "";
                videos = new Array();
    
                // Initialize the videos array using the given matches,
                // set up the HTML to display corresponding thumbnails 
                // and video details.
                for (i = 0; i < Math.min(videoURLs.length, 5); i++) {
                    var questionMarkPosition = videoURLs[i].indexOf("?v=");
                    var videoId;
    
                    if (questionMarkPosition >= 0) {
                        videoId = videoURLs[i].substr(
                            questionMarkPosition + 3, 11);
                    }
                    else {
                        videoId = videoURLs[i].substr(16, 11);
                    }
    
                    videos[i] = { "Id": videoId };
    
    
                    // Dynamically create the HTML to display the  
                    // thumbnail and play the video upon the onclick event.
                    html += "<div class='thumbnailFrame' id='" + 
                        videos[i].Id + "frame'><img class='thumbnail' 
                        id='" + videos[i].Id + "' width='32' height='32' 
                        onclick='selectVideo(" + i + ");'/></div>";
    
                    loadVideoDetails(i);
                }
    
                document.getElementById("thumbnails").innerHTML = html;
    
    
               // If there is only one video URL in the item body,  
               // don't bother with displaying the thumbnail.
                if (videos.length == 1) {
                    document.getElementById(
                        "thumbnails").style.display = "none";
                }
    
                // Select the first video by default, set up the 
                // YouTube embedded player for that video.
                selectVideo(0);
            }
    
    
  • Defining the function loadVideoDetails—This function dynamically creates a new HTML script element, initializes its src attribute to use a Google Data web service to obtain details for the video, and specifies the videosDetailsLoaded function as the callback method. This function then places the script element at the end of the head section on the HTML page.

            // Dynamically create the HTML to load the details for the
            // specified video.
            function loadVideoDetails(videoIndex) {
                // Dynamically create a new HTML SCRIPT element 
                // in the webpage.
                var script = document.createElement("script");
                // Specify the URL to retrieve the indicated video 
                // from a feed of a current list of videos, as the 
                //value of the src attribute of the SCRIPT element.
                script.setAttribute("src", 
                   "https://gdata.youtube.com/feeds/api/videos/" + 
                   videos[videoIndex].Id + 
                   "?alt=json-in-script&callback=videoDetailsLoaded");
                // Insert the SCRIPT element at the end of the HEAD section.
                document.getElementsByTagName('head')[0].appendChild(script);
            }
    
    
  • Defining the function videoDetailsLoaded—Upon receiving a web service response for the thumbnail URL and details for a video, this callback function sets the corresponding structure in the videos array with this information. The web service response is a JSON-format feed.

            // Callback function to which the response for the video 
            // search is sent. The input argument videoFeed is a 
            // JSON-format feed.
            function videoDetailsLoaded(videoFeed) {
                var videoIndex = 
                getVideoIndex(videoFeed.entry.id.$t.substring(42));
    
                if (videoFeed.entry.media$group.media$thumbnail.length >
                    0) {
                    videos[videoIndex].ThumbnailURL = 
                    videoFeed.entry.media$group.media$thumbnail[0].url.replace(
                    "http:", "https:");
    
                    document.getElementById(videos[videoIndex].Id).src = 
                    videos[videoIndex].ThumbnailURL;
                }
    
                videos[videoIndex].Title = videoFeed.entry.title.$t;
                videos[videoIndex].PublishedDate = 
                    parseDate(videoFeed.entry.published.$t);
                videos[videoIndex].Description = 
                    videoFeed.entry.media$group.media$description.$t;
                videos[videoIndex].ViewCount = 
                    parseInt(videoFeed.entry.yt$statistics.viewCount);
    
                if (videoIndex == selectedVideo) {
                    refreshVideoDetails(selectedVideo);
                }
            }
    
  • Defining the function getVideoIndex—This function returns the index of the video in the videos array, whose Id field matches the input argument videoId from the JSON object.

            // Gets the array index of the video whose Id field matches 
            // videoId from the web service response.
            function getVideoIndex(videoId) {
                for (i = 0; i < videos.length; i++) {
                    if (videos[i].Id == videoId) {
                        return i;
                    }
                }
    
                return null;
            }
    
    
  • Defining the function parseDate—This function parses the date string that is obtained from the JSON object and creates a Date variable that consists of the appropriate year, month, and day values.

            function parseDate(dateString) {
                var year = parseInt(dateString.substring(0, 4));
                var month = parseInt(dateString.substring(5, 7));
                var day = parseInt(dateString.substring(8, 10));
    
                var result = new Date();
                result.setUTCFullYear(year);
                result.setUTCMonth(month - 1);
                result.setUTCDate(day);
    
                return result;
            }
    
    
  • Defining the function selectVideo—This function takes as an input parameter the array index of one of the YouTube videos returned from the VideoURL regular expression, sets up the YouTube embedded player for that video, and refreshes the details by calling refreshVideoDetails.

            // Based on the index for the selected video, set up the  
            // YouTube embedded player, and refresh the details for 
            // that video.
            function selectVideo(videoIndex) {
                selectedVideo = videoIndex;
    
                // Change the background of each video in the videos array  
                // to white background. For the selected video, change the 
                // background to black.
                for (i = 0; i < videos.length; i++) {
                    document.getElementById(
                        videos[i].Id + "frame").style.background =
                        i == videoIndex ? "Black" : "White";
                }
    
                // Set the HTML for the embeddedvideo iframe to use  
                // the YouTube embedded player to play the corresponding
                // video.
                document.getElementById("embeddedVideo").innerHTML = 
                    "<iframe width='354' height='200' frameborder='0' 
                    src='https://www.youtube.com/embed/" + 
                    videos[videoIndex].Id + "?html5=True'/>";
                
                refreshVideoDetails(videoIndex);
            }
    
    
  • Defining the function refreshVideoDetails—Using the initialized structure in the videos array for the specified video, this function forms the HTML and displays the video details.

    
            // Form the HTML and display details about the specified video.
            function refreshVideoDetails(videoIndex) {
                var html = "";
    
                if (videos[videoIndex].Title != undefined) {
                    html += "<p class='videoTitle'>" + 
                        videos[videoIndex].Title + "</p>";
                }
    
                if (videos[videoIndex].Description != undefined) {
                    html += "<p class='multiLineVideoDetails'>" +
                        videos[videoIndex].Description + "</p>";
                }
    
                if (videos[videoIndex].PublishedDate != undefined) {
                    html += "<p class='singleLineVideoDetails' 
                        style='margin-top: 8px;'>" + String.format 
                        (addedOn, 
                        videos[videoIndex].PublishedDate.toLocaleDateString())
                        + "</p>";
                }
    
                if (videos[videoIndex].ViewCount != undefined) {
                    html += "<p class='singleLineVideoDetails'>" + 
                        String.format(viewCount, 
                        videos[videoIndex].ViewCount) + "</p>";
                }
    
                document.getElementById("details").innerHTML = html;
            }
    
    

See Appendix B: HTML file for a complete listing of the HTML file.

The following is a listing of the XML manifest for the YouTube sample mail app.

<?xml version="1.0" encoding="utf-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:type="MailApp">
  <Id>971E76EF-D73E-567F-ADAE-5A76B39052CF</Id>
  <Version>1.0</Version>
  <ProviderName>Microsoft</ProviderName>
  <DefaultLocale>en-us</DefaultLocale>
  <DisplayName DefaultValue="YouTube"/>
  <Description DefaultValue=
  "Watch YouTube videos referenced in the e-mails you receive without 
leaving your email client.">
    <Override Locale="fr-fr" Value="Visualisez les vidéos YouTube 
référencées dans vos courriers électronique directement depuis 
Outlook et Outlook Web App."/>
  </Description>
  <!-- Change the following line to specify the web server -->
  <!-- that hosts the icon file. -->
  <IconUrl DefaultValue="https://webserver/YouTube/YouTubeLogo.png"/>
  <Capabilities>
    <Capability Name="Mailbox"/>
  </Capabilities>
  <DesktopSettings>
    <!-- Change the following line to specify the web server -->
    <!-- that hosts the HTML file. -->
    <SourceLocation DefaultValue=
    "https://webserver/YouTube/YouTube.htm"/>
    <RequestedHeight>216</RequestedHeight>
  </DesktopSettings>
  <TabletSettings>
    <!-- Change the following line to specify the web server -->
    <!-- that hosts the HTML file. -->
    <SourceLocation DefaultValue=
    "https://webserver/YouTube/YouTube.htm"/>
    <RequestedHeight>216</RequestedHeight>
  </TabletSettings>
  <Permissions>ReadItem</Permissions>
  <Rule xsi:type="ItemHasRegularExpressionMatch" 
  PropertyName="BodyAsPlaintext" RegExName="VideoURL" RegExValue=
  "http://(((www\.)?youtube\.com/watch\?v=)|(youtu\.be/))[a-zA-Z0-9_-]{11}"
  />
</OfficeApp>

The following listing shows the HTML and JavaScript code for the YouTube sample app.

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" >
    <title>YouTube extension</title>
    <style type="text/css">
        *
        {
            margin: 0px;
            overflow: hidden;
        }
        .thumbnailFrame
        {
            margin-bottom: 4px;
        }
        .thumbnail
        {
            margin-right: 4px;
        }
        .videoTitle
        {
            font-family: Segoe UI;
            font-size: 16px;
        }
        .multiLineVideoDetails
        {
            font-family: Segoe UI;
            font-size: 14px;
            color: Gray;
        }
        .singleLineVideoDetails
        {
            font-family: Segoe UI;
            font-size: 12px;
            color: Gray;
        }
    </style>
    <script src="MicrosoftAjax.js" type="text/javascript"></script>
    <!-- Use the CDN reference to Office.js. -->
    <script src="https://appsforoffice.microsoft.com/lib/1.0/hosted/Office.js"
        type="text/javascript"></script>
    <script src="strings_en-us.js" type="text/javascript"></script>
    <script type="text/javascript">
        var videos;
        var selectedVideo = -1;

        function parseDate(dateString) {
            var year = parseInt(dateString.substring(0, 4));
            var month = parseInt(dateString.substring(5, 7));
            var day = parseInt(dateString.substring(8, 10));

            var result = new Date();
            result.setUTCFullYear(year);
            result.setUTCMonth(month - 1);
            result.setUTCDate(day);

            return result;
        }

        function getVideoIndex(videoId) {
            for (i = 0; i < videos.length; i++) {
                if (videos[i].Id == videoId) {
                    return i;
                }
            }

            return null;
        }

        function videoDetailsLoaded(videoFeed) {
            var videoIndex = getVideoIndex(videoFeed.entry.id.$t.substring(42));

            if (videoFeed.entry.media$group.media$thumbnail.length > 0) {
                videos[videoIndex].ThumbnailURL = videoFeed.entry.media$group.media$thumbnail[0].url.replace("http:", "https:");

                document.getElementById(videos[videoIndex].Id).src = videos[videoIndex].ThumbnailURL;
            }

            videos[videoIndex].Title = videoFeed.entry.title.$t;
            videos[videoIndex].PublishedDate = parseDate(videoFeed.entry.published.$t);
            videos[videoIndex].Description = videoFeed.entry.media$group.media$description.$t;
            videos[videoIndex].ViewCount = parseInt(videoFeed.entry.yt$statistics.viewCount);

            if (videoIndex == selectedVideo) {
                refreshVideoDetails(selectedVideo);
            }
        }

        function loadVideoDetails(videoIndex) {
            var script = document.createElement("script");
            script.setAttribute("src", "https://gdata.youtube.com/feeds/api/videos/" + videos[videoIndex].Id + "?alt=json-in-script&callback=videoDetailsLoaded");
            document.getElementsByTagName('head')[0].appendChild(script);
        }

        function refreshVideoDetails(videoIndex) {
            var html = "";

            if (videos[videoIndex].Title != undefined) {
                html += "<p class='videoTitle'>" + videos[videoIndex].Title + "</p>";
            }

            if (videos[videoIndex].Description != undefined) {
                html += "<p class='multiLineVideoDetails'>" + videos[videoIndex].Description + "</p>";
            }

            if (videos[videoIndex].PublishedDate != undefined) {
                html += "<p class='singleLineVideoDetails' style='margin-top: 8px;'>" + String.format(addedOn, videos[videoIndex].PublishedDate.toLocaleDateString()) + "</p>";
            }

            if (videos[videoIndex].ViewCount != undefined) {
                html += "<p class='singleLineVideoDetails'>" + String.format(viewCount, videos[videoIndex].ViewCount) + "</p>";
            }

            document.getElementById("details").innerHTML = html;
        }

        function selectVideo(videoIndex) {
            selectedVideo = videoIndex;

            for (i = 0; i < videos.length; i++) {
                document.getElementById(videos[i].Id + "frame").style.background = i == videoIndex ? "Black" : "White";
            }

            // document.getElementById("embeddedVideo").innerHTML = "<iframe width='354' height='200' frameborder='0' src='https://www.youtube.com/embed/" + videos[videoIndex].Id + "?autohide=1&showinfo=0'/>";
            document.getElementById("embeddedVideo").innerHTML = "<iframe width='354' height='200' frameborder='0' src='https://www.youtube.com/embed/" + videos[videoIndex].Id + "?html5=True'/>";
            
            refreshVideoDetails(videoIndex);
        }

        function init(videoURLs) {
            var html = "";
            videos = new Array();

            for (i = 0; i < Math.min(videoURLs.length, 5); i++) {
                var questionMarkPosition = videoURLs[i].indexOf("?v=");
                var videoId;

                if (questionMarkPosition >= 0) {
                    videoId = videoURLs[i].substr(questionMarkPosition + 3, 11);
                }
                else {
                    videoId = videoURLs[i].substr(16, 11);
                }

                videos[i] = { "Id": videoId };

                html += "<div class='thumbnailFrame' id='" + videos[i].Id + "frame'><img class='thumbnail' id='" + videos[i].Id + "' width='32' height='32' onclick='selectVideo(" + i + ");'/></div>";

                loadVideoDetails(i);
            }

            document.getElementById("thumbnails").innerHTML = html;

            if (videos.length == 1) {
                document.getElementById("thumbnails").style.display = "none";
            }

            selectVideo(0);
        }

        Office.initialize = function (reason) {
            init(Office.context.mailbox.item.getRegExMatches().VideoURL);
        }
    </script>
</head>
<body style="margin: 8px">
    <table cellpadding="0px" cellspacing="0px" style="table-layout: fixed" height="200px">
        <tr>
            <td id="thumbnails" valign="top" />
            <td id="embeddedVideo" valign="top" />
            <td valign="top">
                <div id="details" style="margin-left: 10px; height: 200px; overflow: auto" />
            </td>
        </tr>
    </table>
</body>
</html>

Show:
© 2014 Microsoft