You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何通过Tin Can API跟踪PDF阅读并插入代码发送xAPI Statement至LRS?

Great question! You’re absolutely right that Tin Can API (now widely known as xAPI) excels at tracking nearly every type of learning activity—including PDF reading. And yes, you can absolutely use JavaScript or C# to send xAPI statements directly to a Learning Record Store (LRS). Let’s walk through how to implement this, with practical examples for both languages.

Core Basics: xAPI Statements

Before diving into code, remember that every xAPI statement needs three mandatory components:

  • Actor: The person performing the activity (e.g., a user’s email or unique ID)
  • Verb: The action taken (must use a standard xAPI Verb IRI, like http://adlnet.gov/expapi/verbs/viewed or http://adlnet.gov/expapi/verbs/completed)
  • Object: The activity being tracked (your PDF, identified by a unique URL/IRI, with a type of http://adlnet.gov/expapi/activities/resource)

All statements are sent as JSON via a POST request to your LRS’s /statements endpoint, usually with Basic Authentication.

Implementation 1: JavaScript (Web-Hosted PDFs)

For PDFs embedded in a web page, JavaScript is the go-to choice. We’ll use Mozilla’s pdf.js library to render the PDF and hook into reading events (like page turns or scroll progress).

Step 1: Set Up PDF.js

First, include the PDF.js library in your page:

<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>

Step 2: Render the PDF & Track Events

Here’s a complete example that tracks when a user navigates to a new page and sends an xAPI statement:

// Configuration
const pdfUrl = "https://your-domain.com/path/to/your/document.pdf";
const lrsEndpoint = "https://your-lrs-url.com/xapi/statements";
const lrsUsername = "your-lrs-username";
const lrsPassword = "your-lrs-password";

// Initialize PDF.js
const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

async function renderPDF() {
  const pdf = await pdfjsLib.getDocument(pdfUrl).promise;
  const pageContainer = document.getElementById('pdf-container');
  
  // Render first page
  let currentPage = 1;
  await renderPage(currentPage);

  // Track page changes (e.g., when user clicks next/prev or scrolls)
  document.getElementById('next-page').addEventListener('click', async () => {
    if (currentPage < pdf.numPages) {
      currentPage++;
      await renderPage(currentPage);
      sendXapiStatement(`viewed page ${currentPage} of ${pdf.numPages}`);
    }
  });

  document.getElementById('prev-page').addEventListener('click', async () => {
    if (currentPage > 1) {
      currentPage--;
      await renderPage(currentPage);
      sendXapiStatement(`viewed page ${currentPage} of ${pdf.numPages}`);
    }
  });
}

// Helper to render a single page
async function renderPage(pageNum) {
  const page = await pdf.getPage(pageNum);
  const viewport = page.getViewport({ scale: 1.5 });
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');
  canvas.height = viewport.height;
  canvas.width = viewport.width;
  
  const pageContainer = document.getElementById('pdf-container');
  pageContainer.innerHTML = '';
  pageContainer.appendChild(canvas);
  
  await page.render({ canvasContext: context, viewport: viewport }).promise;
}

// Helper to send xAPI statement to LRS
async function sendXapiStatement(activityDescription) {
  const statement = {
    actor: {
      mbox: "mailto:user@your-domain.com", // Replace with user's actual email/ID
      name: "John Doe"
    },
    verb: {
      id: "http://adlnet.gov/expapi/verbs/viewed",
      display: { "en-US": "viewed" }
    },
    object: {
      id: pdfUrl,
      definition: {
        name: { "en-US": "Your PDF Document Title" },
        description: { "en-US": activityDescription },
        type: "http://adlnet.gov/expapi/activities/resource"
      }
    }
  };

  // Encode credentials for Basic Auth
  const credentials = btoa(`${lrsUsername}:${lrsPassword}`);
  
  try {
    const response = await fetch(lrsEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${credentials}`
      },
      body: JSON.stringify(statement)
    });

    if (!response.ok) {
      console.error('Failed to send xAPI statement:', await response.text());
    } else {
      console.log('xAPI statement sent successfully!');
    }
  } catch (error) {
    console.error('Error sending xAPI statement:', error);
  }
}

// Start rendering
renderPDF();

Bonus: Track Completion

To track when a user finishes the PDF, add a check when they reach the last page, and send a statement with the completed verb (http://adlnet.gov/expapi/verbs/completed).

Implementation 2: C# (Desktop/Backend)

If you’re building a desktop app (e.g., WPF, WinForms) or need to send statements from a backend service, C# works perfectly. We’ll use HttpClient to send the POST request.

Example Code

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json; // Use Newtonsoft.Json or System.Text.Json

namespace XapiPdfTracker
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // Configuration
            string lrsEndpoint = "https://your-lrs-url.com/xapi/statements";
            string lrsUsername = "your-lrs-username";
            string lrsPassword = "your-lrs-password";
            string pdfUrl = "https://your-domain.com/path/to/your/document.pdf";

            // Create xAPI statement
            var statement = new
            {
                actor = new
                {
                    mbox = "mailto:user@your-domain.com",
                    name = "John Doe"
                },
                verb = new
                {
                    id = "http://adlnet.gov/expapi/verbs/viewed",
                    display = new { en_US = "viewed" }
                },
                @object = new // "object" is a keyword in C#, so use @
                {
                    id = pdfUrl,
                    definition = new
                    {
                        name = new { en_US = "Your PDF Document Title" },
                        description = new { en_US = "Completed reading the entire PDF" },
                        type = "http://adlnet.gov/expapi/activities/resource"
                    }
                }
            };

            // Convert statement to JSON
            string jsonStatement = JsonConvert.SerializeObject(statement);

            // Set up HttpClient with Basic Auth
            using (var client = new HttpClient())
            {
                var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{lrsUsername}:{lrsPassword}"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Send POST request
                var content = new StringContent(jsonStatement, Encoding.UTF8, "application/json");
                var response = await client.PostAsync(lrsEndpoint, content);

                if (response.IsSuccessStatusCode)
                {
                    Console.WriteLine("xAPI statement sent successfully!");
                }
                else
                {
                    Console.WriteLine($"Failed to send statement: {response.StatusCode}");
                    Console.WriteLine(await response.Content.ReadAsStringAsync());
                }
            }
        }
    }
}
Key Things to Remember
  • LRS Authentication: Almost all LRS systems require Basic Authentication—make sure your credentials are correctly encoded.
  • Verb & Object Standards: Stick to official xAPI Verb IRIs and use the correct activity type for PDFs to ensure compatibility across systems.
  • Event Throttling: Don’t send a statement for every tiny action (e.g., every scroll pixel). Instead, trigger statements on meaningful events: page turns, completion, or spending X seconds on a page.
  • Error Handling: Always handle failed requests—log errors, retry if appropriate, and inform the user if needed.

内容的提问来源于stack exchange,提问作者Hendri Triwanto

火山引擎 最新活动